[gjs: 2/9] wrapperutils: Factor out Base/Prototype/Instance scheme



commit 6e8ad29483024f711d1656a5ef77ff7239f71b6a
Author: Philip Chimento <philip chimento gmail com>
Date:   Thu Dec 6 21:21:21 2018 -0800

    wrapperutils: Factor out Base/Prototype/Instance scheme
    
    This generalizes the ObjectBase/ObjectPrototype/ObjectInstance classes
    from object.cpp into three template classes in wrapperutils.h:
    GIWrapperBase, GIWrapperPrototype, and GIWrapperInstance.
    
    We want to take up as little space as possible, so all three use the
    CRTP technique (child class as a template parameter) in order to get
    compile-time polymorphism instead of runtime polymorphism where a vtable
    would need to occupy space.
    
    The changes consist mostly of moving code from object.cpp to
    wrapperutils.h and renaming some methods and members to be more general.
    
    Since this code has traditionally been difficult to understand for those
    unfamiliar with the code base, we try to remedy this by adding
    comprehensive documentation comments in wrapperutils.h.

 gi/gobject.cpp    |   2 +-
 gi/object.cpp     | 575 +++++++++-------------------------
 gi/object.h       | 232 ++++----------
 gi/wrapperutils.h | 917 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 1114 insertions(+), 612 deletions(-)
---
diff --git a/gi/gobject.cpp b/gi/gobject.cpp
index 1be87fc9..f9db5c94 100644
--- a/gi/gobject.cpp
+++ b/gi/gobject.cpp
@@ -134,7 +134,7 @@ static GObject* gjs_object_constructor(
     /* We only hold a toggle ref at this point, add back a ref that the
      * native code can own.
      */
-    return G_OBJECT(g_object_ref(priv->to_instance()->gobj()));
+    return G_OBJECT(g_object_ref(priv->to_instance()->ptr()));
 }
 
 static void gjs_object_set_gproperty(GObject* object, unsigned property_id,
diff --git a/gi/object.cpp b/gi/object.cpp
index e35f8aaf..3a82bb38 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -73,9 +73,6 @@ std::stack<JS::PersistentRootedObject> ObjectInstance::object_init_list{};
 static bool weak_pointer_callback = false;
 ObjectInstance *ObjectInstance::wrapped_gobject_list = nullptr;
 
-extern struct JSClass gjs_object_instance_class;
-GJS_DEFINE_PRIV_FROM_JS(ObjectBase, gjs_object_instance_class)
-
 // clang-format off
 G_DEFINE_QUARK(gjs::custom-type, ObjectBase::custom_type)
 G_DEFINE_QUARK(gjs::custom-property, ObjectBase::custom_property)
@@ -162,20 +159,10 @@ void ObjectInstance::unlink(void) {
     m_instance_link.unlink();
 }
 
-GIObjectInfo* ObjectBase::info(void) const { return get_prototype()->m_info; }
-
-GType ObjectBase::gtype(void) const { return get_prototype()->m_gtype; }
-
-const void* ObjectBase::gobj_addr(void) const {
+const JSObject* ObjectBase::jsobj_addr(void) const {
     if (is_prototype())
         return nullptr;
-    return to_instance()->m_gobj;
-}
-
-const void* ObjectBase::jsobj_addr(void) const {
-    if (is_prototype())
-        return nullptr;
-    return to_instance()->m_wrapper.get();
+    return to_instance()->wrapper();
 }
 
 static bool
@@ -188,15 +175,6 @@ throw_priv_is_null_error(JSContext *context)
     return false;
 }
 
-bool ObjectBase::check_is_instance(JSContext* cx, const char* for_what) const {
-    if (!is_prototype())
-        return true;
-    const ObjectPrototype* priv = to_prototype();
-    gjs_throw(cx, "Can't %s on %s.%s.prototype; only on instances", for_what,
-              priv->ns(), priv->name());
-    return false;
-}
-
 bool ObjectInstance::check_gobject_disposed(const char* for_what) const {
     if (!m_gobj_disposed)
         return true;
@@ -206,25 +184,11 @@ bool ObjectInstance::check_gobject_disposed(const char* for_what) const {
         "it. This might be caused by the object having been destroyed from C "
         "code using something such as destroy(), dispose(), or remove() "
         "vfuncs.",
-        ns(), name(), m_gobj, for_what);
+        ns(), name(), m_ptr, for_what);
     gjs_dumpstack();
     return false;
 }
 
-/* Gets the ObjectBase belonging to a particular JS object wrapper. Checks
- * that the wrapper object has the right JSClass (ObjectInstance::klass)
- * and returns null if not. */
-ObjectBase* ObjectBase::for_js(JSContext* cx, JS::HandleObject wrapper) {
-    return priv_from_js(cx, wrapper);
-}
-
-/* Use when you don't have a JSContext* available. This method is infallible
- * and cannot trigger a GC, so it's safe to use from finalize() and trace().
- * (It can return null if no instance has been set yet.) */
-ObjectBase* ObjectBase::for_js_nocheck(JSObject* wrapper) {
-    return static_cast<ObjectBase*>(JS_GetPrivate(wrapper));
-}
-
 ObjectInstance *
 ObjectInstance::for_gobject(GObject *gobj)
 {
@@ -243,9 +207,11 @@ ObjectInstance::check_js_object_finalized(void)
     if (!m_uses_toggle_ref)
         return;
     if (G_UNLIKELY(m_wrapper_finalized)) {
-        g_critical("Object %p (a %s) resurfaced after the JS wrapper was finalized. "
-                   "This is some library doing dubious memory management inside dispose()",
-                   m_gobj, type_name());
+        g_critical(
+            "Object %p (a %s) resurfaced after the JS wrapper was finalized. "
+            "This is some library doing dubious memory management inside "
+            "dispose()",
+            m_ptr, type_name());
         m_wrapper_finalized = false;
         g_assert(!m_wrapper);  /* should associate again with a new wrapper */
     }
@@ -263,13 +229,13 @@ void ObjectPrototype::set_type_qdata(void) {
 void
 ObjectInstance::set_object_qdata(void)
 {
-    g_object_set_qdata(m_gobj, gjs_object_priv_quark(), this);
+    g_object_set_qdata(m_ptr, gjs_object_priv_quark(), this);
 }
 
 void
 ObjectInstance::unset_object_qdata(void)
 {
-    g_object_set_qdata(m_gobj, gjs_object_priv_quark(), nullptr);
+    g_object_set_qdata(m_ptr, gjs_object_priv_quark(), nullptr);
 }
 
 GParamSpec* ObjectPrototype::find_param_spec_from_id(JSContext* cx,
@@ -300,17 +266,6 @@ GParamSpec* ObjectPrototype::find_param_spec_from_id(JSContext* cx,
     return pspec; /* owned by property cache */
 }
 
-/* Gets the ObjectPrototype corresponding to obj.prototype. Cannot return null,
- * and asserts so. */
-ObjectPrototype* ObjectPrototype::for_js_prototype(JSContext* context,
-                                                   JS::HandleObject obj) {
-    JS::RootedObject proto(context);
-    JS_GetPrototype(context, obj, &proto);
-    ObjectBase* retval = ObjectBase::for_js(context, proto);
-    g_assert(retval);
-    return retval->to_prototype();
-}
-
 /* A hook on adding a property to an object. This is called during a set
  * property operation after all the resolve hooks on the prototype chain have
  * failed to resolve. We use this to mark an object as needing toggle refs when
@@ -348,7 +303,7 @@ ObjectInstance::add_property_impl(JSContext       *cx,
 }
 
 bool ObjectBase::prop_getter(JSContext* cx, unsigned argc, JS::Value* vp) {
-    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
 
     JS::RootedString name(cx,
         gjs_dynamic_property_private_slot(&args.callee()).toString());
@@ -390,7 +345,7 @@ bool ObjectInstance::prop_getter_impl(JSContext* cx, JS::HandleString name,
                      param->name);
 
     g_value_init(&gvalue, G_PARAM_SPEC_VALUE_TYPE(param));
-    g_object_get_property(m_gobj, param->name, &gvalue);
+    g_object_get_property(m_ptr, param->name, &gvalue);
     if (!gjs_value_from_g_value(cx, rval, &gvalue)) {
         g_value_unset(&gvalue);
         return false;
@@ -446,7 +401,7 @@ GIFieldInfo* ObjectPrototype::find_field_info_from_id(JSContext* cx,
 }
 
 bool ObjectBase::field_getter(JSContext* cx, unsigned argc, JS::Value* vp) {
-    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
 
     JS::RootedString name(cx,
         gjs_dynamic_property_private_slot(&args.callee()).toString());
@@ -491,7 +446,7 @@ bool ObjectInstance::field_getter_impl(JSContext* cx, JS::HandleString name,
         return false;
     }
 
-    if (!g_field_info_get_field(field, m_gobj, &arg)) {
+    if (!g_field_info_get_field(field, m_ptr, &arg)) {
         gjs_throw(cx, "Error getting field %s from object",
                   gjs_debug_string(name).c_str());
         return false;
@@ -505,7 +460,7 @@ bool ObjectInstance::field_getter_impl(JSContext* cx, JS::HandleString name,
 /* Dynamic setter for GObject properties. Returns false on OOM/exception.
  * args.rval() becomes the "stored value" for the property. */
 bool ObjectBase::prop_setter(JSContext* cx, unsigned argc, JS::Value* vp) {
-    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
 
     JS::RootedString name(cx,
         gjs_dynamic_property_private_slot(&args.callee()).toString());
@@ -553,14 +508,14 @@ bool ObjectInstance::prop_setter_impl(JSContext* cx, JS::HandleString name,
         return false;
     }
 
-    g_object_set_property(m_gobj, param_spec->name, &gvalue);
+    g_object_set_property(m_ptr, param_spec->name, &gvalue);
     g_value_unset(&gvalue);
 
     return true;
 }
 
 bool ObjectBase::field_setter(JSContext* cx, unsigned argc, JS::Value* vp) {
-    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
 
     JS::RootedString name(cx,
         gjs_dynamic_property_private_slot(&args.callee()).toString());
@@ -796,70 +751,27 @@ is_gobject_property_name(GIObjectInfo *info,
     return true;
 }
 
-bool ObjectBase::resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id,
-                         bool* resolved) {
-    auto* priv = ObjectBase::for_js(cx, obj);
-
-    if (!priv) {
-        /* We won't have a private until the initializer is called, so
-         * just defer to prototype chains in this case.
-         *
-         * This isn't too bad: either you get undefined if the field
-         * doesn't exist on any of the prototype chains, or whatever code
-         * will run afterwards will fail because of the "priv == NULL"
-         * check there.
-         */
-        debug_jsprop_static("Resolve hook", id, obj);
-        *resolved = false;
-        return true;
-    }
-
-    if (!priv->is_prototype()) {
-        *resolved = false;
-        return true;
-    }
-
-    return priv->to_prototype()->resolve_impl(cx, obj, id, resolved);
-}
-
-/* The JSResolveOp for an instance is called for every property not
- * defined, even if it's one of the functions or properties we're
- * adding to the proto manually.
- */
-static bool name_is_overridden(jsid name, const GjsAtoms& atoms) {
-    // Keep this list in sync with gjs_object_instance_proto_props and
-    // gjs_object_instance_proto_funcs. However, explicitly do not include
+// Override of GIWrapperBase::id_is_never_lazy()
+bool ObjectBase::id_is_never_lazy(jsid name, const GjsAtoms& atoms) {
+    // Keep this list in sync with ObjectBase::proto_properties and
+    // ObjectBase::proto_methods. However, explicitly do not include
     // connect() in it, because there are a few cases where the lazy property
     // should override the predefined one, such as Gio.Cancellable.connect().
     return name == atoms.init() || name == atoms.connect_after() ||
-           name == atoms.emit() || name == atoms.to_string();
+           name == atoms.emit();
 }
 
 bool ObjectPrototype::resolve_impl(JSContext* context, JS::HandleObject obj,
-                                   JS::HandleId id, bool* resolved) {
-    debug_jsprop("Resolve hook", id, obj);
-
-    if (name_is_overridden(id, GjsContextPrivate::atoms(context))) {
-        *resolved = false;
-        return true;
-    }
-
-    JS::UniqueChars name;
-    if (!gjs_get_string_id(context, id, &name))
-        return false;
-    if (!name) {
-        *resolved = false;
-        return true;  /* not resolved, but no error */
-    }
-
+                                   JS::HandleId id, const char* name,
+                                   bool* resolved) {
     /* If we have no GIRepository information (we're a JS GObject subclass),
      * we need to look at exposing interfaces. Look up our interfaces through
      * GType data, and then hope that *those* are introspectable. */
     if (is_custom_js_class())
-        return resolve_no_info(context, obj, id, resolved, name.get(),
+        return resolve_no_info(context, obj, id, resolved, name,
                                ConsiderMethodsAndProperties);
 
-    if (g_str_has_prefix(name.get(), "vfunc_")) {
+    if (g_str_has_prefix(name, "vfunc_")) {
         /* The only time we find a vfunc info is when we're the base
          * class that defined the vfunc. If we let regular prototype
          * chaining resolve this, we'd have the implementation for the base's
@@ -898,11 +810,10 @@ bool ObjectPrototype::resolve_impl(JSContext* context, JS::HandleObject obj,
          * method resolution. */
     }
 
-    if (is_gobject_property_name(m_info, name.get()))
-        return lazy_define_gobject_property(context, obj, id, resolved,
-                                            name.get());
+    if (is_gobject_property_name(m_info, name))
+        return lazy_define_gobject_property(context, obj, id, resolved, name);
 
-    GjsAutoFieldInfo field_info = lookup_field_info(m_info, name.get());
+    GjsAutoFieldInfo field_info = lookup_field_info(m_info, name);
     if (field_info) {
         bool found = false;
         if (!JS_AlreadyHasOwnPropertyById(context, obj, id, &found))
@@ -920,9 +831,8 @@ bool ObjectPrototype::resolve_impl(JSContext* context, JS::HandleObject obj,
 
         JS::RootedValue private_id(context, JS::StringValue(JSID_TO_STRING(id)));
         if (!gjs_define_property_dynamic(
-                context, obj, name.get(), "gobject_field",
-                &ObjectBase::field_getter, &ObjectBase::field_setter,
-                private_id, flags))
+                context, obj, name, "gobject_field", &ObjectBase::field_getter,
+                &ObjectBase::field_setter, private_id, flags))
             return false;
 
         *resolved = true;
@@ -941,7 +851,7 @@ bool ObjectPrototype::resolve_impl(JSContext* context, JS::HandleObject obj,
      */
 
     GjsAutoFunctionInfo method_info =
-        g_object_info_find_method_using_interfaces(m_info, name.get(), nullptr);
+        g_object_info_find_method_using_interfaces(m_info, name, nullptr);
 
     /**
      * Search through any interfaces implemented by the GType;
@@ -949,7 +859,7 @@ bool ObjectPrototype::resolve_impl(JSContext* context, JS::HandleObject obj,
      * https://bugzilla.gnome.org/show_bug.cgi?id=632922
      */
     if (!method_info)
-        return resolve_no_info(context, obj, id, resolved, name.get(),
+        return resolve_no_info(context, obj, id, resolved, name,
                                ConsiderOnlyMethods);
 
 #if GJS_VERBOSE_ENABLE_GI_USAGE
@@ -972,27 +882,9 @@ bool ObjectPrototype::resolve_impl(JSContext* context, JS::HandleObject obj,
     return true;
 }
 
-bool ObjectBase::new_enumerate(JSContext        *cx,
-                               JS::HandleObject  obj,
-                               JS::AutoIdVector &properties,
-                               bool              only_enumerable) {
-    auto* priv = ObjectBase::for_js(cx, obj);
-
-    if (!priv->is_prototype()) {
-        // Instances don't have any methods or properties.
-        // Spidermonkey will call new_enumerate on the prototype next.
-        return true;
-    }
-
-    return priv->to_prototype()->new_enumerate_impl(cx, obj, properties,
-                                                    only_enumerable);
-}
-
 bool ObjectPrototype::new_enumerate_impl(JSContext* cx, JS::HandleObject obj,
                                          JS::AutoIdVector& properties,
                                          bool only_enumerable) {
-    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Enumerate %s", type_name());
-
     unsigned n_interfaces;
     GType* interfaces = g_type_interfaces(gtype(), &n_interfaces);
 
@@ -1375,10 +1267,10 @@ ObjectInstance::release_native_object(void)
 {
     discard_wrapper();
     if (m_uses_toggle_ref)
-        g_object_remove_toggle_ref(m_gobj, wrapped_gobj_toggle_notify, nullptr);
+        g_object_remove_toggle_ref(m_ptr, wrapped_gobj_toggle_notify, nullptr);
     else
-        g_object_unref(m_gobj);
-    m_gobj = nullptr;
+        g_object_unref(m_ptr);
+    m_ptr = nullptr;
 }
 
 /* At shutdown, we need to ensure we've cleared the context of any
@@ -1411,41 +1303,16 @@ gjs_object_prepare_shutdown(void)
         std::mem_fn(&ObjectInstance::release_native_object));
 }
 
-ObjectInstance *
-ObjectInstance::new_for_js_object(JSContext       *cx,
-                                  JS::HandleObject obj)
-{
-    ObjectInstance *priv = g_slice_new0(ObjectInstance);
-    new (priv) ObjectInstance(cx, obj);
-    return priv;
-}
-
 ObjectInstance::ObjectInstance(JSContext* cx, JS::HandleObject object)
-    : ObjectBase(ObjectPrototype::for_js_prototype(cx, object)) {
-    g_assert(!JS_GetPrivate(object));
-
+    : GIWrapperInstance(cx, object) {
     GJS_INC_COUNTER(object_instance);
-    debug_lifecycle("Instance constructor");
-}
-
-ObjectPrototype* ObjectPrototype::create(JSContext* cx, GIObjectInfo* info,
-                                         GType gtype) {
-    auto* priv = g_slice_new0(ObjectPrototype);
-    new (priv) ObjectPrototype(info, gtype);
-    if (!priv->init(cx))
-        return nullptr;
-    return priv;
 }
 
 ObjectPrototype::ObjectPrototype(GIObjectInfo* info, GType gtype)
-    : ObjectBase(), m_info(info), m_gtype(gtype) {
-    if (info)
-        g_base_info_ref(info);
-
+    : GIWrapperPrototype(info, gtype) {
     g_type_class_ref(gtype);
 
     GJS_INC_COUNTER(object_prototype);
-    debug_lifecycle("Prototype constructor");
 }
 
 bool ObjectPrototype::init(JSContext* cx) {
@@ -1505,7 +1372,7 @@ ObjectInstance::associate_js_gobject(JSContext       *context,
     g_assert(!wrapper_is_rooted());
 
     m_uses_toggle_ref = false;
-    m_gobj = gobj;
+    m_ptr = gobj;
     set_object_qdata();
     m_wrapper = object;
 
@@ -1538,12 +1405,12 @@ ObjectInstance::ensure_uses_toggle_ref(JSContext *cx)
      */
     m_uses_toggle_ref = true;
     switch_to_rooted(cx);
-    g_object_add_toggle_ref(m_gobj, wrapped_gobj_toggle_notify, nullptr);
+    g_object_add_toggle_ref(m_ptr, wrapped_gobj_toggle_notify, nullptr);
 
     /* We now have both a ref and a toggle ref, we only want the toggle ref.
      * This may immediately remove the GC root we just added, since refcount
      * may drop to 1. */
-    g_object_unref(m_gobj);
+    g_object_unref(m_ptr);
 }
 
 void ObjectBase::invalidate_all_closures(void) {
@@ -1565,13 +1432,15 @@ ObjectInstance::disassociate_js_gobject(void)
     bool had_toggle_down, had_toggle_up;
 
     if (!m_gobj_disposed)
-        g_object_weak_unref(m_gobj, wrapped_gobj_dispose_notify, this);
+        g_object_weak_unref(m_ptr, wrapped_gobj_dispose_notify, this);
 
     auto& toggle_queue = ToggleQueue::get_default();
-    std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(m_gobj);
+    std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(m_ptr);
     if (had_toggle_down != had_toggle_up) {
-        g_error("JS object wrapper for GObject %p (%s) is being released while "
-                "toggle references are still pending.", m_gobj, type_name());
+        g_error(
+            "JS object wrapper for GObject %p (%s) is being released while "
+            "toggle references are still pending.",
+            m_ptr, type_name());
     }
 
     /* Fist, remove the wrapper pointer from the wrapped GObject */
@@ -1648,53 +1517,26 @@ ObjectInstance::init_impl(JSContext              *context,
         /* we should already have a ref */
     }
 
-    if (!m_gobj)
+    if (!m_ptr)
         associate_js_gobject(context, object, gobj);
 
     debug_lifecycle("JSObject created");
 
-    TRACE(GJS_OBJECT_WRAPPER_NEW(this, m_gobj, ns(), name()));
+    TRACE(GJS_OBJECT_WRAPPER_NEW(this, m_ptr, ns(), name()));
 
     args.rval().setObject(*object);
     return true;
 }
 
-GJS_NATIVE_CONSTRUCTOR_DECLARE(object_instance)
-{
-    GJS_NATIVE_CONSTRUCTOR_VARIABLES(object_instance)
-    bool ret;
+// See GIWrapperBase::constructor()
+bool ObjectInstance::constructor_impl(JSContext* context,
+                                      JS::HandleObject object,
+                                      const JS::CallArgs& argv) {
     JS::RootedValue initer(context);
-
-    GJS_NATIVE_CONSTRUCTOR_PRELUDE(object_instance);
-
-    /* Init the private variable before we do anything else. If a garbage
-     * collection happens when calling the init function then this object
-     * might be traced and we will end up dereferencing a null pointer */
-    JS_SetPrivate(object, ObjectInstance::new_for_js_object(context, object));
-
     GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context);
-    if (!gjs_object_require_property(context, object, "GObject instance",
-                                     gjs->atoms().init(), &initer))
-        return false;
-
-    argv.rval().setUndefined();
-    ret = gjs->call_function(object, initer, argv, argv.rval());
-
-    if (argv.rval().isUndefined())
-        argv.rval().setObject(*object);
-
-    return ret;
-}
-
-void ObjectBase::trace(JSTracer* tracer, JSObject* obj) {
-    auto* priv = ObjectBase::for_js_nocheck(obj);
-    if (priv == NULL)
-        return;
-
-    if (priv->is_prototype())
-        priv->to_prototype()->trace_impl(tracer);
-    else
-        priv->trace_impl(tracer);
+    return gjs_object_require_property(context, object, "GObject instance",
+                                       gjs->atoms().init(), &initer) &&
+           gjs->call_function(object, initer, argv, argv.rval());
 }
 
 void ObjectBase::trace_impl(JSTracer* tracer) {
@@ -1705,38 +1547,19 @@ void ObjectBase::trace_impl(JSTracer* tracer) {
 void ObjectPrototype::trace_impl(JSTracer* tracer) {
     m_property_cache.trace(tracer);
     m_field_cache.trace(tracer);
-    ObjectBase::trace_impl(tracer);
-}
-
-void ObjectBase::finalize(JSFreeOp* fop, JSObject* obj) {
-    auto* priv = ObjectBase::for_js_nocheck(obj);
-    g_assert (priv != NULL);
-
-    if (priv->is_prototype()) {
-        priv->to_prototype()->~ObjectPrototype();
-        g_slice_free(ObjectPrototype, priv);
-    } else {
-        priv->to_instance()->~ObjectInstance();
-        g_slice_free(ObjectInstance, priv);
-    }
-
-    /* Remove the pointer from the JSObject */
-    JS_SetPrivate(obj, nullptr);
 }
 
 ObjectInstance::~ObjectInstance() {
-    debug_lifecycle("Finalize");
-
-    TRACE(GJS_OBJECT_WRAPPER_FINALIZE(this, m_gobj, ns(), name()));
+    TRACE(GJS_OBJECT_WRAPPER_FINALIZE(this, m_ptr, ns(), name()));
 
     invalidate_all_closures();
 
     /* GObject is not already freed */
-    if (m_gobj) {
+    if (m_ptr) {
         bool had_toggle_up;
         bool had_toggle_down;
 
-        if (G_UNLIKELY (m_gobj->ref_count <= 0)) {
+        if (G_UNLIKELY(m_ptr->ref_count <= 0)) {
             g_error(
                 "Finalizing wrapper for an already freed object of type: "
                 "%s.%s\n",
@@ -1744,7 +1567,7 @@ ObjectInstance::~ObjectInstance() {
         }
 
         auto& toggle_queue = ToggleQueue::get_default();
-        std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(m_gobj);
+        std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(m_ptr);
 
         if (!had_toggle_up && had_toggle_down) {
             g_error(
@@ -1754,7 +1577,7 @@ ObjectInstance::~ObjectInstance() {
         }
 
         if (!m_gobj_disposed)
-            g_object_weak_unref(m_gobj, wrapped_gobj_dispose_notify, this);
+            g_object_weak_unref(m_ptr, wrapped_gobj_dispose_notify, this);
         release_native_object();
     }
 
@@ -1876,10 +1699,7 @@ void ObjectBase::closure_invalidated_notify(void* data, GClosure* closure) {
 }
 
 bool ObjectBase::connect(JSContext* cx, unsigned argc, JS::Value* vp) {
-    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
-    if (!priv)
-        return throw_priv_is_null_error(cx); /* wrong class passed in */
-
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
     if (!priv->check_is_instance(cx, "connect to signals"))
         return false;
 
@@ -1887,10 +1707,7 @@ bool ObjectBase::connect(JSContext* cx, unsigned argc, JS::Value* vp) {
 }
 
 bool ObjectBase::connect_after(JSContext* cx, unsigned argc, JS::Value* vp) {
-    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
-    if (!priv)
-        return throw_priv_is_null_error(cx); /* wrong class passed in */
-
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
     if (!priv->check_is_instance(cx, "connect to signals"))
         return false;
 
@@ -1937,11 +1754,8 @@ ObjectInstance::connect_impl(JSContext          *context,
         return false;
     associate_closure(context, closure);
 
-    id = g_signal_connect_closure_by_id(m_gobj,
-                                        signal_id,
-                                        signal_detail,
-                                        closure,
-                                        after);
+    id = g_signal_connect_closure_by_id(m_ptr, signal_id, signal_detail,
+                                        closure, after);
 
     args.rval().setDouble(id);
 
@@ -1949,10 +1763,7 @@ ObjectInstance::connect_impl(JSContext          *context,
 }
 
 bool ObjectBase::emit(JSContext* cx, unsigned argc, JS::Value* vp) {
-    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
-    if (!priv)
-        return throw_priv_is_null_error(cx); /* wrong class passed in */
-
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
     if (!priv->check_is_instance(cx, "emit signal"))
         return false;
 
@@ -2005,8 +1816,8 @@ ObjectInstance::emit_impl(JSContext          *context,
     instance_and_args = g_newa(GValue, signal_query.n_params + 1);
     memset(instance_and_args, 0, sizeof(GValue) * (signal_query.n_params + 1));
 
-    g_value_init(&instance_and_args[0], G_TYPE_FROM_INSTANCE(m_gobj));
-    g_value_set_instance(&instance_and_args[0], m_gobj);
+    g_value_init(&instance_and_args[0], gtype());
+    g_value_set_instance(&instance_and_args[0], m_ptr);
 
     failed = false;
     for (i = 0; i < signal_query.n_params; ++i) {
@@ -2045,30 +1856,50 @@ ObjectInstance::emit_impl(JSContext          *context,
 }
 
 bool ObjectBase::to_string(JSContext* cx, unsigned argc, JS::Value* vp) {
-    GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
-    if (!priv)
-        return throw_priv_is_null_error(cx);  /* wrong class passed in */
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
+    return gjs_wrapper_to_string_func(
+        cx, obj, priv->to_string_kind(), priv->info(), priv->gtype(),
+        priv->is_prototype() ? nullptr : priv->to_instance()->ptr(),
+        args.rval());
+}
 
-    if (priv->is_prototype())
-        return priv->to_prototype()->to_string_impl(cx, args);
-    return priv->to_instance()->to_string_impl(cx, args);
+// Override of GIWrapperBase::to_string_kind()
+const char* ObjectBase::to_string_kind(void) const {
+    if (is_prototype())
+        return "object";
+    return to_instance()->to_string_kind();
 }
 
-bool
-ObjectInstance::to_string_impl(JSContext          *cx,
-                               const JS::CallArgs& args)
-{
-    return gjs_wrapper_to_string_func(
-        cx, m_wrapper, m_gobj_disposed ? "object (FINALIZED)" : "object",
-        info(), gtype(), m_gobj, args.rval());
+/*
+ * ObjectInstance::to_string_kind:
+ *
+ * Instance-only version of GIWrapperBase::to_string_kind(). ObjectInstance
+ * shows a "finalized" marker in its toString() method if the wrapped GObject
+ * has already been finalized.
+ */
+const char* ObjectInstance::to_string_kind(void) const {
+    return m_gobj_disposed ? "object (FINALIZED)" : "object";
 }
 
-bool ObjectPrototype::to_string_impl(JSContext* cx, const JS::CallArgs& args) {
-    return gjs_wrapper_to_string_func(cx, nullptr, "object prototype", info(),
-                                      gtype(), nullptr, args.rval());
+/*
+ * ObjectBase::init_gobject:
+ *
+ * This is named "init_gobject()" but corresponds to "_init()" in JS. The reason
+ * for the name is that an "init()" method is used within SpiderMonkey to
+ * indicate fallible initialization that must be done before an object can be
+ * used, which is not the case here.
+ */
+bool ObjectBase::init_gobject(JSContext* context, unsigned argc,
+                              JS::Value* vp) {
+    GJS_GET_WRAPPER_PRIV(context, argc, vp, argv, obj, ObjectBase, priv);
+    if (!priv->check_is_instance(context, "initialize"))
+        return false;
+
+    return priv->to_instance()->init_impl(context, argv, &obj);
 }
 
-static const struct JSClassOps gjs_object_class_ops = {
+// clang-format off
+const struct JSClassOps ObjectBase::class_ops = {
     &ObjectBase::add_property,
     nullptr,  // deleteProperty
     nullptr,  // enumerate
@@ -2082,38 +1913,32 @@ static const struct JSClassOps gjs_object_class_ops = {
     &ObjectBase::trace,
 };
 
-struct JSClass gjs_object_instance_class = {
+const struct JSClass ObjectBase::klass = {
     "GObject_Object",
     JSCLASS_HAS_PRIVATE | JSCLASS_FOREGROUND_FINALIZE,
-    &gjs_object_class_ops
-};
-
-bool ObjectBase::init(JSContext* context, unsigned argc, JS::Value* vp) {
-    GJS_GET_THIS(context, argc, vp, argv, obj);
-
-    if (!do_base_typecheck(context, obj, true))
-        return false;
-
-    auto* priv = ObjectBase::for_js(context, obj);
-    g_assert(priv);  /* Already checked by do_base_typecheck() */
-
-    if (!priv->check_is_instance(context, "initialize"))
-        return false;
-
-    return priv->to_instance()->init_impl(context, argv, &obj);
-}
-
-JSPropertySpec gjs_object_instance_proto_props[] = {
-    JS_PS_END
+    &ObjectBase::class_ops
 };
 
-JSFunctionSpec gjs_object_instance_proto_funcs[] = {
-    JS_FN("_init", &ObjectBase::init, 0, 0),
+JSFunctionSpec ObjectBase::proto_methods[] = {
+    JS_FN("_init", &ObjectBase::init_gobject, 0, 0),
     JS_FN("connect", &ObjectBase::connect, 0, 0),
     JS_FN("connect_after", &ObjectBase::connect_after, 0, 0),
     JS_FN("emit", &ObjectBase::emit, 0, 0),
-    JS_FN("toString", &ObjectBase::to_string, 0, 0),
-    JS_FS_END};
+    JS_FS_END
+};
+// clang-format on
+
+// Override of GIWrapperPrototype::get_parent_proto()
+bool ObjectPrototype::get_parent_proto(JSContext* cx,
+                                       JS::MutableHandleObject proto) const {
+    GType parent_type = g_type_parent(gtype());
+    if (parent_type != G_TYPE_INVALID) {
+        proto.set(gjs_lookup_object_prototype(cx, parent_type));
+        if (!proto)
+            return false;
+    }
+    return true;
+}
 
 bool
 gjs_define_object_class(JSContext              *context,
@@ -2123,77 +1948,8 @@ gjs_define_object_class(JSContext              *context,
                         JS::MutableHandleObject constructor,
                         JS::MutableHandleObject prototype)
 {
-    const char *constructor_name;
-    JS::RootedObject parent_proto(context);
-
-    const char *ns;
-    GType parent_type;
-
-    g_assert(in_object);
-    g_assert(gtype != G_TYPE_INVALID);
-
-    /*   http://egachine.berlios.de/embedding-sm-best-practice/apa.html
-     *   http://www.sitepoint.com/blogs/2006/01/17/javascript-inheritance/
-     *   http://www.cs.rit.edu/~atk/JavaScript/manuals/jsobj/
-     *
-     * What we want is:
-     *
-     * repoobj.Gtk.Window is constructor for a GtkWindow wrapper JSObject
-     *   (gjs_define_object_constructor() is supposed to define Window in Gtk)
-     *
-     * Window.prototype contains the methods on Window, e.g. set_default_size()
-     * mywindow.__proto__ is Window.prototype
-     * mywindow.__proto__.__proto__ is Bin.prototype
-     * mywindow.__proto__.__proto__.__proto__ is Container.prototype
-     *
-     * Because Window.prototype is an instance of Window in a sense,
-     * Window.prototype.__proto__ is Window.prototype, just as
-     * mywindow.__proto__ is Window.prototype
-     *
-     * If we do "mywindow = new Window()" then we should get:
-     *     mywindow.__proto__ == Window.prototype
-     * which means "mywindow instanceof Window" is true.
-     *
-     * Remember "Window.prototype" is "the __proto__ of stuff
-     * constructed with new Window()"
-     *
-     * __proto__ is used to search for properties if you do "this.foo"
-     * while __parent__ defines the scope to search if you just have
-     * "foo".
-     *
-     * __proto__ is used to look up properties, while .prototype is only
-     * relevant for constructors and is used to set __proto__ on new'd
-     * objects. So .prototype only makes sense on constructors.
-     *
-     * JS_SetPrototype() and JS_GetPrototype() are for __proto__.
-     * To set/get .prototype, just use the normal property accessors,
-     * or JS_InitClass() sets it up automatically.
-     *
-     * JavaScript is SO AWESOME
-     */
-
-    parent_type = g_type_parent(gtype);
-    if (parent_type != G_TYPE_INVALID)
-        parent_proto = gjs_lookup_object_prototype(context, parent_type);
-
-    ns = gjs_get_names_from_gtype_and_gi_info(gtype, (GIBaseInfo *) info,
-                                              &constructor_name);
-
-    if (!gjs_init_class_dynamic(context, in_object,
-                                parent_proto,
-                                ns, constructor_name,
-                                &gjs_object_instance_class,
-                                gjs_object_instance_constructor, 0,
-                                /* props of prototype */
-                                parent_proto ? NULL : &gjs_object_instance_proto_props[0],
-                                /* funcs of prototype */
-                                parent_proto ? NULL : &gjs_object_instance_proto_funcs[0],
-                                /* props of constructor, MyConstructor.myprop */
-                                NULL,
-                                /* funcs of constructor, MyConstructor.myfunc() */
-                                NULL,
-                                prototype,
-                                constructor))
+    if (!ObjectPrototype::create_class(context, in_object, info, gtype,
+                                       constructor, prototype))
         return false;
 
     /* Hook_up_vfunc can't be included in gjs_object_instance_proto_funcs
@@ -2204,21 +1960,12 @@ gjs_define_object_class(JSContext              *context,
                                GJS_MODULE_PROP_FLAGS))
         return false;
 
-    auto* priv = ObjectPrototype::create(context, info, gtype);
-    if (!priv)
-        return false;
-    JS_SetPrivate(prototype, priv);
-
-    gjs_debug(GJS_DEBUG_GOBJECT, "Defined class for %s (%s), prototype %p, "
-              "JSClass %p, in object %p", constructor_name, g_type_name(gtype),
-              prototype.get(), JS_GetClass(prototype), in_object.get());
-
     if (info)
         if (!gjs_define_static_methods<InfoType::Object>(context, constructor,
                                                          gtype, info))
             return false;
 
-    return gjs_wrapper_define_gtype_prop(context, constructor, gtype);
+    return true;
 }
 
 JSObject*
@@ -2251,7 +1998,6 @@ gjs_object_from_g_object(JSContext    *context,
             return nullptr;
 
         priv = ObjectInstance::new_for_js_object(context, obj);
-        JS_SetPrivate(obj, priv);
 
         g_object_ref_sink(gobj);
         priv->associate_js_gobject(context, obj, gobj);
@@ -2277,7 +2023,7 @@ gjs_g_object_from_object(JSContext       *cx,
     if (!instance->check_gobject_disposed("access"))
         return nullptr;
 
-    return instance->gobj();
+    return instance->ptr();
 }
 
 bool
@@ -2285,61 +2031,27 @@ gjs_typecheck_is_object(JSContext       *context,
                         JS::HandleObject object,
                         bool             throw_error)
 {
-    return do_base_typecheck(context, object, throw_error);
+    if (throw_error)
+        return !!ObjectBase::for_js_typecheck(context, object);
+    return !!ObjectBase::for_js(context, object);
 }
 
 bool gjs_typecheck_object(JSContext* cx, JS::HandleObject object,
                           GType expected_type, bool throw_error) {
-    if (!do_base_typecheck(cx, object, throw_error))
-        return false;
-
-    auto* priv = ObjectBase::for_js(cx, object);
-
-    if (priv == NULL) {
-        if (throw_error)
+    if (throw_error) {
+        if (!ObjectBase::typecheck(cx, object, nullptr, expected_type))
             return throw_priv_is_null_error(cx);
-
-        return false;
+        return true;
     }
-
-    /* check_is_instance() throws, so only call if if throw_error.
-     * is_prototype() checks the same thing without throwing. */
-    if ((throw_error && !priv->check_is_instance(cx, "convert to GObject*")) ||
-        priv->is_prototype())
-        return false;
-
-    return priv->to_instance()->typecheck_object(cx, expected_type,
-                                                 throw_error);
+    return ObjectBase::typecheck(cx, object, nullptr, expected_type,
+                                 ObjectBase::TypecheckNoThrow());
 }
 
-bool
-ObjectInstance::typecheck_object(JSContext *context,
-                                 GType      expected_type,
-                                 bool       throw_error)
-{
-    g_assert(m_gobj_disposed || gtype() == G_OBJECT_TYPE(m_gobj));
-
-    bool result;
-    if (expected_type != G_TYPE_NONE)
-        result = g_type_is_a(gtype(), expected_type);
-    else
-        result = true;
-
-    if (!result && throw_error) {
-        if (!is_custom_js_class()) {
-            gjs_throw_custom(context, JSProto_TypeError, nullptr,
-                             "Object is of type %s.%s - cannot convert to %s",
-                             ns(), name(),
-                             g_type_name(expected_type));
-        } else {
-            gjs_throw_custom(context, JSProto_TypeError, nullptr,
-                             "Object is of type %s - cannot convert to %s",
-                             type_name(),
-                             g_type_name(expected_type));
-        }
-    }
-
-    return result;
+// Overrides GIWrapperInstance::typecheck_impl()
+bool ObjectInstance::typecheck_impl(JSContext* cx, GIBaseInfo* expected_info,
+                                    GType expected_type) const {
+    g_assert(m_gobj_disposed || gtype() == G_OBJECT_TYPE(m_ptr));
+    return GIWrapperInstance::typecheck_impl(cx, expected_info, expected_type);
 }
 
 GJS_JSAPI_RETURN_CONVENTION
@@ -2401,10 +2113,7 @@ static bool find_vfunc_info(JSContext* context, GType implementor_gtype,
 }
 
 bool ObjectBase::hook_up_vfunc(JSContext* cx, unsigned argc, JS::Value* vp) {
-    GJS_GET_PRIV(cx, argc, vp, args, prototype, ObjectBase, priv);
-    if (!priv)
-        return throw_priv_is_null_error(cx);
-
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, prototype, ObjectBase, priv);
     /* Normally we wouldn't assert is_prototype(), but this method can only be
      * called internally so it's OK to crash if done wrongly */
     return priv->to_prototype()->hook_up_vfunc_impl(cx, args, prototype);
diff --git a/gi/object.h b/gi/object.h
index bf7932a3..bf64edcd 100644
--- a/gi/object.h
+++ b/gi/object.h
@@ -32,6 +32,7 @@
 #include <stack>
 #include <vector>
 
+#include "gi/wrapperutils.h"
 #include "gjs/jsapi-util-root.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/jsapi-wrapper.h"
@@ -64,147 +65,52 @@ struct AutoGValueVector : public std::vector<GValue> {
 
 class ObjectPrototype;
 
-/* To conserve memory, we have two different kinds of private data for GObject
- * JS wrappers: ObjectInstance, and ObjectPrototype. Both inherit from
- * ObjectBase for their common functionality.
+/*
+ * ObjectBase:
+ *
+ * Specialization of GIWrapperBase for GObject instances. See the documentation
+ * in wrapperutils.h.
  *
  * It's important that ObjectBase and ObjectInstance not grow in size without a
  * very good reason. There can be tens, maybe hundreds of thousands of these
  * objects alive in a typical gnome-shell run, so even 8 more bytes will add up.
  * It's less critical that ObjectPrototype stay small, since only one of these
  * is allocated per GType.
- *
- * Sadly, we cannot have virtual methods in ObjectBase, because SpiderMonkey can
- * be compiled with or without RTTI, so we cannot count on being able to cast
- * ObjectBase to ObjectInstance or ObjectPrototype with dynamic_cast<>, and the
- * vtable would take up just as much space anyway. Instead, we have the
- * to_prototype() and to_instance() methods which will give you a pointer if the
- * ObjectBase is of the correct type (and assert if not.)
  */
-class ObjectBase {
- protected:
-    /* nullptr if this is an ObjectPrototype; points to the corresponding
-     * ObjectPrototype if this is an ObjectInstance */
-    ObjectPrototype* m_proto;
+class ObjectBase
+    : public GIWrapperBase<ObjectBase, ObjectPrototype, ObjectInstance> {
+    friend class GIWrapperBase;
 
+ protected:
     /* a list of all GClosures installed on this object (from
      * signals, trampolines, explicit GClosures, and vfuncs on prototypes),
      * used when tracing */
     std::forward_list<GClosure*> m_closures;
 
-    explicit ObjectBase(ObjectPrototype* proto = nullptr) : m_proto(proto) {}
-
-    /* Methods to get an existing ObjectBase */
+    explicit ObjectBase(ObjectPrototype* proto = nullptr)
+        : GIWrapperBase(proto) {}
 
  public:
-    GJS_USE
-    static ObjectBase* for_js(JSContext* cx, JS::HandleObject obj);
-    GJS_USE
-    static ObjectBase* for_js_nocheck(JSObject* obj);
+    static const GjsDebugTopic debug_topic = GJS_DEBUG_GOBJECT;
+    static constexpr const char* debug_tag = "GObject";
 
-    /* Methods for getting a pointer to the correct subclass. We don't use
-     * standard C++ subclasses because that would occupy another 8 bytes in
-     * ObjectInstance for a vtable. */
-
-    GJS_USE
-    bool is_prototype(void) const { return !m_proto; }
-
-    /* The to_instance() and to_prototype() methods assert that this ObjectBase
-     * is of the correct subclass. If you don't want an assert, then either
-     * check beforehand or use get_prototype(). */
-
-    GJS_USE
-    ObjectPrototype* to_prototype(void) {
-        g_assert(is_prototype());
-        return reinterpret_cast<ObjectPrototype*>(this);
-    }
-    GJS_USE
-    const ObjectPrototype* to_prototype(void) const {
-        g_assert(is_prototype());
-        return reinterpret_cast<const ObjectPrototype*>(this);
-    }
-    GJS_USE
-    ObjectInstance* to_instance(void) {
-        g_assert(!is_prototype());
-        return reinterpret_cast<ObjectInstance*>(this);
-    }
-    GJS_USE
-    const ObjectInstance* to_instance(void) const {
-        g_assert(!is_prototype());
-        return reinterpret_cast<const ObjectInstance*>(this);
-    }
-
-    /* get_prototype() doesn't assert. If you call it on an ObjectPrototype, it
-     * returns you the same object cast to the correct type; if you call it on
-     * an ObjectInstance, it returns you the ObjectPrototype belonging to the
-     * corresponding JS prototype. */
-    GJS_USE
-    ObjectPrototype* get_prototype(void) {
-        return is_prototype() ? to_prototype() : m_proto;
-    }
-    GJS_USE
-    const ObjectPrototype* get_prototype(void) const {
-        return is_prototype() ? to_prototype() : m_proto;
-    }
-
-    /* Accessors */
-
-    /* Both ObjectInstance and ObjectPrototype have GIObjectInfo and GType,
-     * but for space reasons we store it only on ObjectPrototype. */
-    GJS_USE GIObjectInfo* info(void) const;
-    GJS_USE GType gtype(void) const;
-
-    GJS_USE
-    const char* ns(void) const {
-        return info() ? g_base_info_get_namespace(info()) : "";
-    }
-    GJS_USE
-    const char* name(void) const {
-        return info() ? g_base_info_get_name(info()) : type_name();
-    }
-    GJS_USE
-    const char* type_name(void) const { return g_type_name(gtype()); }
-    GJS_USE
-    bool is_custom_js_class(void) const { return !info(); }
+    static const struct JSClassOps class_ops;
+    static const struct JSClass klass;
+    static JSFunctionSpec proto_methods[];
 
  private:
-    /* These are used in debug methods only. */
-    GJS_USE const void* gobj_addr(void) const;
-    GJS_USE const void* jsobj_addr(void) const;
+    // This is used in debug methods only.
+    GJS_USE const JSObject* jsobj_addr(void) const;
 
     /* Helper methods */
 
- public:
-    GJS_JSAPI_RETURN_CONVENTION
-    bool check_is_instance(JSContext* cx, const char* for_what) const;
-
  protected:
     void debug_lifecycle(const char* message) const {
-        gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
-                            "[%p: GObject %p JS wrapper %p %s.%s (%s)] %s",
-                            this, gobj_addr(), jsobj_addr(), ns(), name(),
-                            type_name(), message);
-    }
-    void debug_jsprop_base(const char* message, const char* id,
-                           JSObject* obj) const {
-        gjs_debug_jsprop(GJS_DEBUG_GOBJECT,
-                         "[%p: GObject %p JS object %p %s.%s (%s)] %s '%s'",
-                         this, gobj_addr(), obj, ns(), name(), type_name(),
-                         message, id);
-    }
-    void debug_jsprop(const char* message, jsid id, JSObject* obj) const {
-        debug_jsprop_base(message, gjs_debug_id(id).c_str(), obj);
-    }
-    void debug_jsprop(const char* message, JSString* id, JSObject* obj) const {
-        debug_jsprop_base(message, gjs_debug_string(id).c_str(), obj);
-    }
-    static void debug_jsprop_static(const char* message, jsid id,
-                                    JSObject* obj) {
-        gjs_debug_jsprop(GJS_DEBUG_GOBJECT,
-                         "[JS object %p] %s '%s', no instance associated", obj,
-                         message, gjs_debug_id(id).c_str());
+        GIWrapperBase::debug_lifecycle(jsobj_addr(), message);
     }
 
+    bool id_is_never_lazy(jsid name, const GjsAtoms& atoms);
+
  public:
     void type_query_dynamic_safe(GTypeQuery* query);
 
@@ -221,16 +127,8 @@ class ObjectBase {
 
     static bool add_property(JSContext* cx, JS::HandleObject obj,
                              JS::HandleId id, JS::HandleValue value);
-    static bool resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id,
-                        bool* resolved);
-    static bool new_enumerate(
-        JSContext* cx, JS::HandleObject obj,
-        JS::AutoIdVector& properties,  // NOLINT(runtime/references)
-        bool only_enumerable);
-    static void finalize(JSFreeOp* fop, JSObject* obj);
-    static void trace(JSTracer* tracer, JSObject* obj);
 
- protected:
+ private:
     void trace_impl(JSTracer* tracer);
 
     /* JS property getters/setters */
@@ -255,8 +153,9 @@ class ObjectBase {
     static bool emit(JSContext* cx, unsigned argc, JS::Value* vp);
     GJS_JSAPI_RETURN_CONVENTION
     static bool to_string(JSContext* cx, unsigned argc, JS::Value* vp);
+    GJS_USE const char* to_string_kind(void) const;
     GJS_JSAPI_RETURN_CONVENTION
-    static bool init(JSContext* cx, unsigned argc, JS::Value* vp);
+    static bool init_gobject(JSContext* cx, unsigned argc, JS::Value* vp);
     GJS_JSAPI_RETURN_CONVENTION
     static bool hook_up_vfunc(JSContext* cx, unsigned argc, JS::Value* vp);
 
@@ -266,10 +165,10 @@ class ObjectBase {
     GJS_USE static GQuark custom_property_quark(void);
 };
 
-class ObjectPrototype : public ObjectBase {
-    // ObjectBase needs to call private methods (such as trace_impl) because
-    // of the unusual inheritance scheme
-    friend class ObjectBase;
+class ObjectPrototype
+    : public GIWrapperPrototype<ObjectBase, ObjectPrototype, ObjectInstance> {
+    friend class GIWrapperPrototype;
+    friend class GIWrapperBase;
 
     using PropertyCache =
         JS::GCHashMap<JS::Heap<JSString*>, GjsAutoParam,
@@ -278,9 +177,6 @@ class ObjectPrototype : public ObjectBase {
         JS::GCHashMap<JS::Heap<JSString*>, GjsAutoInfo<GI_INFO_TYPE_FIELD>,
                       js::DefaultHasher<JSString*>, js::SystemAllocPolicy>;
 
-    GIObjectInfo* m_info;
-    GType m_gtype;
-
     PropertyCache m_property_cache;
     FieldCache m_field_cache;
 
@@ -289,23 +185,14 @@ class ObjectPrototype : public ObjectBase {
     ~ObjectPrototype();
 
  public:
-    /* Public constructor for instances (uses GSlice allocator) */
-    GJS_USE
-    static ObjectPrototype* create(JSContext* cx, GIObjectInfo* info,
-                                   GType gtype);
-
-    GJS_USE
-    static ObjectPrototype* for_js(JSContext* cx, JS::HandleObject obj) {
-        return ObjectBase::for_js(cx, obj)->to_prototype();
-    }
     GJS_USE
     static ObjectPrototype* for_gtype(GType gtype);
-    GJS_USE
-    static ObjectPrototype* for_js_prototype(JSContext* cx,
-                                             JS::HandleObject obj);
 
     /* Helper methods */
  private:
+    GJS_JSAPI_RETURN_CONVENTION
+    bool get_parent_proto(JSContext* cx, JS::MutableHandleObject proto) const;
+
     GJS_USE
     bool is_vfunc_unchanged(GIVFuncInfo* info);
     GJS_JSAPI_RETURN_CONVENTION
@@ -345,7 +232,7 @@ class ObjectPrototype : public ObjectBase {
  private:
     GJS_JSAPI_RETURN_CONVENTION
     bool resolve_impl(JSContext* cx, JS::HandleObject obj, JS::HandleId id,
-                      bool* resolved);
+                      const char* prop_name, bool* resolved);
 
     GJS_JSAPI_RETURN_CONVENTION
     bool new_enumerate_impl(
@@ -355,25 +242,20 @@ class ObjectPrototype : public ObjectBase {
     void trace_impl(JSTracer* tracer);
 
     /* JS methods */
- private:
-    GJS_JSAPI_RETURN_CONVENTION
-    bool to_string_impl(JSContext* cx, const JS::CallArgs& args);
+ public:
     GJS_JSAPI_RETURN_CONVENTION
     bool hook_up_vfunc_impl(JSContext* cx, const JS::CallArgs& args,
                             JS::HandleObject prototype);
-
-    ObjectPrototype(const ObjectPrototype& other) = delete;
-    ObjectPrototype(ObjectPrototype&& other) = delete;
-    ObjectPrototype& operator=(const ObjectPrototype& other) = delete;
-    ObjectPrototype& operator=(ObjectPrototype&& other) = delete;
 };
 
-class ObjectInstance : public ObjectBase {
-    // ObjectBase needs to call private methods (such as trace_impl) because
-    // of the unusual inheritance scheme
-    friend class ObjectBase;
+class ObjectInstance : public GIWrapperInstance<ObjectBase, ObjectPrototype,
+                                                ObjectInstance, GObject> {
+    friend class GIWrapperInstance;
+    friend class GIWrapperBase;
+    friend class ObjectBase;  // for add_property, prop_getter, etc.
+
+    // GIWrapperInstance::m_ptr may be null in ObjectInstance.
 
-    GObject* m_gobj;  // may be null
     GjsMaybeOwned<JSObject*> m_wrapper;
 
     GjsListLink m_instance_link;
@@ -395,12 +277,9 @@ class ObjectInstance : public ObjectBase {
     ObjectInstance(JSContext* cx, JS::HandleObject obj);
     ~ObjectInstance();
 
- public:
-    /* Public constructor for instances (uses GSlice allocator) */
-    GJS_USE
-    static ObjectInstance* new_for_js_object(JSContext* cx,
-                                             JS::HandleObject obj);
+    // Extra method to get an existing ObjectInstance from qdata
 
+ public:
     GJS_USE
     static ObjectInstance* for_gobject(GObject* gobj);
 
@@ -411,8 +290,6 @@ class ObjectInstance : public ObjectBase {
     bool has_wrapper(void) const { return !!m_wrapper; }
 
  public:
-    GJS_USE
-    GObject* gobj(void) const { return m_gobj; }
     GJS_USE
     JSObject* wrapper(void) const { return m_wrapper; }
 
@@ -476,7 +353,6 @@ class ObjectInstance : public ObjectBase {
     GJS_JSAPI_RETURN_CONVENTION
     bool add_property_impl(JSContext* cx, JS::HandleObject obj, JS::HandleId id,
                            JS::HandleValue value);
-    void finalize_impl(JSFreeOp* fop, JSObject* obj);
 
     /* JS property getters/setters */
 
@@ -494,6 +370,12 @@ class ObjectInstance : public ObjectBase {
     bool field_setter_impl(JSContext* cx, JS::HandleString name,
                            JS::HandleValue value);
 
+    // JS constructor
+
+    GJS_JSAPI_RETURN_CONVENTION
+    bool constructor_impl(JSContext* cx, JS::HandleObject obj,
+                          const JS::CallArgs& args);
+
     /* JS methods */
 
  private:
@@ -502,25 +384,19 @@ class ObjectInstance : public ObjectBase {
     GJS_JSAPI_RETURN_CONVENTION
     bool emit_impl(JSContext* cx, const JS::CallArgs& args);
     GJS_JSAPI_RETURN_CONVENTION
-    bool to_string_impl(JSContext* cx, const JS::CallArgs& args);
-    GJS_JSAPI_RETURN_CONVENTION
     bool init_impl(JSContext* cx, const JS::CallArgs& args,
                    JS::MutableHandleObject obj);
+    GJS_USE const char* to_string_kind(void) const;
 
-    /* Methods connected to "public" API */
- public:
-    GJS_USE
-    bool typecheck_object(JSContext* cx, GType expected_type, bool throw_error);
+    GJS_JSAPI_RETURN_CONVENTION
+    bool typecheck_impl(JSContext* cx, GIBaseInfo* expected_info,
+                        GType expected_type) const;
 
     /* Notification callbacks */
 
+ public:
     void gobj_dispose_notify(void);
     void context_dispose_notify(void);
-
-    ObjectInstance(const ObjectInstance& other) = delete;
-    ObjectInstance(ObjectInstance&& other) = delete;
-    ObjectInstance& operator=(const ObjectInstance& other) = delete;
-    ObjectInstance& operator=(ObjectInstance&& other) = delete;
 };
 
 G_BEGIN_DECLS
diff --git a/gi/wrapperutils.h b/gi/wrapperutils.h
index b121a647..dccdcf1e 100644
--- a/gi/wrapperutils.h
+++ b/gi/wrapperutils.h
@@ -1,6 +1,7 @@
 /* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
 /*
  * Copyright (c) 2008  litl, LLC
+ * Copyright (c) 2018  Philip Chimento <philip chimento gmail com>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to
@@ -27,6 +28,7 @@
 #include "gjs/context-private.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/macros.h"
+#include "util/log.h"
 
 G_BEGIN_DECLS
 
@@ -62,4 +64,919 @@ template <InfoType::Tag>
 GJS_JSAPI_RETURN_CONVENTION bool gjs_define_static_methods(
     JSContext* cx, JS::HandleObject constructor, GType gtype, GIBaseInfo* info);
 
+/*
+ * GJS_GET_WRAPPER_PRIV:
+ * @cx: JSContext pointer passed into JSNative function
+ * @argc: Number of arguments passed into JSNative function
+ * @vp: Argument value array passed into JSNative function
+ * @args: Name for JS::CallArgs variable defined by this code snippet
+ * @thisobj: Name for JS::RootedObject variable referring to function's this
+ * @type: Type of private data
+ * @priv: Name for private data variable defined by this code snippet
+ *
+ * A convenience macro for getting the private data from GJS classes using
+ * GIWrapper.
+ * Throws an error and returns false if the 'this' object is not the right type.
+ * Use in any JSNative function.
+ */
+#define GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, thisobj, type, priv) \
+    GJS_GET_THIS(cx, argc, vp, args, thisobj);                        \
+    type* priv = type::for_js_typecheck(cx, thisobj, args);           \
+    if (!priv)                                                        \
+        return false;
+
+/*
+ * GIWrapperBase:
+ *
+ * In most different kinds of C pointer that we expose to JS through GObject
+ * Introspection (boxed, fundamental, gerror, interface, object, union), we want
+ * to have different private structures for the prototype JS object and the JS
+ * objects representing instances. Both should inherit from a base structure for
+ * their common functionality.
+ *
+ * This is mainly for memory reasons. We need to keep track of the GIBaseInfo*
+ * and GType for each dynamically created class, but we don't need to duplicate
+ * that information (16 bytes on x64 systems) for every instance. In some cases
+ * there can also be other information that's only used on the prototype.
+ *
+ * So, to conserve memory, we split the private structures in FooInstance and
+ * FooPrototype, which both inherit from FooBase. All the repeated code in these
+ * structures lives in GIWrapperBase, GIWrapperPrototype, and GIWrapperInstance.
+ *
+ * The m_proto member needs a bit of explanation, as this is used to implement
+ * an unusual form of polymorphism. Sadly, we cannot have virtual methods in
+ * FooBase, because SpiderMonkey can be compiled with or without RTTI, so we
+ * cannot count on being able to cast FooBase to FooInstance or FooPrototype
+ * with dynamic_cast<>, and the vtable would take up just as much space anyway.
+ * Instead, we use the CRTP technique, and distinguish between FooInstance and
+ * FooPrototype using the m_proto member, which will be null for FooPrototype.
+ * Instead of casting, we have the to_prototype() and to_instance() methods
+ * which will give you a pointer if the FooBase is of the correct type (and
+ * assert if not.)
+ *
+ * The CRTP requires inheriting classes to declare themselves friends of the
+ * parent class, so that the parent class can call their private methods.
+ *
+ * For more information about the CRTP, the Wikipedia article is informative:
+ * https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
+ */
+template <class Base, class Prototype, class Instance>
+class GIWrapperBase {
+ protected:
+    // nullptr if this Base is a Prototype; points to the corresponding
+    // Prototype if this Base is an Instance.
+    Prototype* m_proto;
+
+    explicit GIWrapperBase(Prototype* proto = nullptr) : m_proto(proto) {}
+    ~GIWrapperBase(void) {}
+
+    // These three can be overridden in subclasses. See define_jsclass().
+    static constexpr JSPropertySpec* proto_properties = nullptr;
+    static constexpr JSFunctionSpec* proto_methods = nullptr;
+    static constexpr JSFunctionSpec* static_methods = nullptr;
+
+    // Methods to get an existing Base
+
+ public:
+    /*
+     * GIWrapperBase::for_js:
+     *
+     * Gets the Base belonging to a particular JS object wrapper. Checks that
+     * the wrapper object has the right JSClass (Base::klass) and returns null
+     * if not. */
+    GJS_USE
+    static Base* for_js(JSContext* cx, JS::HandleObject wrapper) {
+        JSAutoRequest ar(cx);
+        return static_cast<Base*>(
+            JS_GetInstancePrivate(cx, wrapper, &Base::klass, nullptr));
+    }
+
+    /*
+     * GIWrapperBase::for_js_typecheck:
+     *
+     * Like for_js(), only throws a JS exception if the wrapper object has the
+     * wrong class. Use in JSNative functions, where you have access to a
+     * JS::CallArgs. The exception message will mention args.callee.
+     *
+     * The second overload can be used when you don't have access to an
+     * instance of JS::CallArgs. The exception message will be generic.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static Base* for_js_typecheck(
+        JSContext* cx, JS::HandleObject wrapper,
+        JS::CallArgs& args) {  // NOLINT(runtime/references)
+        JSAutoRequest ar(cx);
+        return static_cast<Base*>(
+            JS_GetInstancePrivate(cx, wrapper, &Base::klass, &args));
+    }
+    GJS_JSAPI_RETURN_CONVENTION
+    static Base* for_js_typecheck(JSContext* cx, JS::HandleObject wrapper) {
+        if (!gjs_typecheck_instance(cx, wrapper, &Base::klass, true))
+            return nullptr;
+        return for_js(cx, wrapper);
+    }
+
+    /*
+     * GIWrapperBase::for_js_nocheck:
+     *
+     * Use when you don't have a JSContext* available. This method is infallible
+     * and cannot trigger a GC, so it's safe to use from finalize() and trace().
+     * (It can return null if no private data has been set yet on the wrapper.)
+     */
+    GJS_USE
+    static Base* for_js_nocheck(JSObject* wrapper) {
+        return static_cast<Base*>(JS_GetPrivate(wrapper));
+    }
+
+    // Methods implementing our CRTP polymorphism scheme follow below. We don't
+    // use standard C++ polymorphism because that would occupy another 8 bytes
+    // for a vtable.
+
+    /*
+     * GIWrapperBase::is_prototype:
+     *
+     * Returns whether this Base is actually a Prototype (true) or an Instance
+     * (false).
+     */
+    GJS_USE bool is_prototype(void) const { return !m_proto; }
+
+    /*
+     * GIWrapperBase::to_prototype:
+     * GIWrapperBase::to_instance:
+     *
+     * These methods assert that this Base is of the correct subclass. If you
+     * don't want to assert, then either check beforehand with is_prototype(),
+     * or use get_prototype().
+     */
+    GJS_USE
+    Prototype* to_prototype(void) {
+        g_assert(is_prototype());
+        return reinterpret_cast<Prototype*>(this);
+    }
+    GJS_USE
+    const Prototype* to_prototype(void) const {
+        g_assert(is_prototype());
+        return reinterpret_cast<const Prototype*>(this);
+    }
+    GJS_USE
+    Instance* to_instance(void) {
+        g_assert(!is_prototype());
+        return reinterpret_cast<Instance*>(this);
+    }
+    GJS_USE
+    const Instance* to_instance(void) const {
+        g_assert(!is_prototype());
+        return reinterpret_cast<const Instance*>(this);
+    }
+
+    /*
+     * GIWrapperBase::get_prototype:
+     *
+     * get_prototype() doesn't assert. If you call it on a Prototype, it returns
+     * you the same object cast to the correct type; if you call it on an
+     * Instance, it returns you the Prototype belonging to the corresponding JS
+     * prototype.
+     */
+    GJS_USE
+    Prototype* get_prototype(void) {
+        return is_prototype() ? to_prototype() : m_proto;
+    }
+    GJS_USE
+    const Prototype* get_prototype(void) const {
+        return is_prototype() ? to_prototype() : m_proto;
+    }
+
+    // Accessors for Prototype members follow below. Both Instance and Prototype
+    // should be able to access the GIFooInfo and the GType, but for space
+    // reasons we store them only on Prototype.
+
+    GJS_USE GIBaseInfo* info(void) const { return get_prototype()->info(); }
+    GJS_USE GType gtype(void) const { return get_prototype()->gtype(); }
+
+    // The next four methods are operations derived from the GIFooInfo.
+
+    GJS_USE bool is_custom_js_class(void) const { return !info(); }
+    GJS_USE const char* type_name(void) const { return g_type_name(gtype()); }
+    GJS_USE
+    const char* ns(void) const {
+        return is_custom_js_class() ? "" : g_base_info_get_namespace(info());
+    }
+    GJS_USE
+    const char* name(void) const {
+        return is_custom_js_class() ? type_name()
+                                    : g_base_info_get_name(info());
+    }
+
+ private:
+    // Accessor for Instance member. Used only in debug methods and toString().
+    GJS_USE
+    const void* ptr_addr(void) const {
+        return is_prototype() ? nullptr : to_instance()->ptr();
+    }
+
+    // Debug methods
+
+ protected:
+    void debug_lifecycle(const char* message) const {
+        gjs_debug_lifecycle(
+            Base::debug_topic, "[%p: %s pointer %p - %s.%s (%s)] %s", this,
+            Base::debug_tag, ptr_addr(), ns(), name(), type_name(), message);
+    }
+    void debug_lifecycle(const JSObject* obj, const char* message) const {
+        gjs_debug_lifecycle(
+            Base::debug_topic,
+            "[%p: %s pointer %p - JS wrapper %p - %s.%s (%s)] %s", this,
+            Base::debug_tag, ptr_addr(), obj, ns(), name(), type_name(),
+            message);
+    }
+    void debug_jsprop(const char* message, const char* id,
+                      const JSObject* obj) const {
+        gjs_debug_jsprop(
+            Base::debug_topic,
+            "[%p: %s pointer %p - JS wrapper %p - %s.%s (%s)] %s '%s'", this,
+            Base::debug_tag, ptr_addr(), obj, ns(), name(), type_name(),
+            message, id);
+    }
+    void debug_jsprop(const char* message, jsid id, const JSObject* obj) const {
+        debug_jsprop(message, gjs_debug_id(id).c_str(), obj);
+    }
+    void debug_jsprop(const char* message, JSString* id,
+                      const JSObject* obj) const {
+        debug_jsprop(message, gjs_debug_string(id).c_str(), obj);
+    }
+    static void debug_jsprop_static(const char* message, jsid id,
+                                    const JSObject* obj) {
+        gjs_debug_jsprop(Base::debug_topic,
+                         "[%s JS wrapper %p] %s '%s', no instance associated",
+                         Base::debug_tag, obj, message,
+                         gjs_debug_id(id).c_str());
+    }
+
+    // JS class operations, used only in the JSClassOps struct
+
+    /*
+     * GIWrapperBase::new_enumerate:
+     *
+     * Include this in the Base::klass vtable if the class should support
+     * lazy enumeration (listing all of the lazy properties that can be defined
+     * in resolve().) If it is included, then there must be a corresponding
+     * Prototype::new_enumerate_impl() method.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool new_enumerate(
+        JSContext* cx, JS::HandleObject obj,
+        JS::AutoIdVector& properties,  // NOLINT(runtime/references)
+        bool only_enumerable) {
+        Base* priv = Base::for_js(cx, obj);
+
+        priv->debug_jsprop("Enumerate hook", "(all)", obj);
+
+        if (!priv->is_prototype()) {
+            // Instances don't have any methods or properties.
+            // Spidermonkey will call new_enumerate on the prototype next.
+            return true;
+        }
+
+        return priv->to_prototype()->new_enumerate_impl(cx, obj, properties,
+                                                        only_enumerable);
+    }
+
+ private:
+    /*
+     * GIWrapperBase::id_is_never_lazy:
+     *
+     * Returns true if @id should never be treated as a lazy property. The
+     * JSResolveOp for an instance is called for every property not defined,
+     * even if it's one of the functions or properties we're adding to the
+     * prototype manually, such as toString().
+     *
+     * Override this and chain up if you have Base::resolve in your JSClassOps
+     * vtable, and have overridden Base::proto_properties or
+     * Base::proto_methods. You should add any identifiers in the override that
+     * you have added to the prototype object.
+     */
+    GJS_USE
+    static bool id_is_never_lazy(jsid id, const GjsAtoms& atoms) {
+        // toString() is always defined somewhere on the prototype chain, so it
+        // is never a lazy property.
+        return id == atoms.to_string();
+    }
+
+ protected:
+    /*
+     * GIWrapperBase::resolve:
+     *
+     * Include this in the Base::klass vtable if the class should support lazy
+     * properties. If it is included, then there must be a corresponding
+     * Prototype::resolve_impl() method.
+     *
+     * The *resolved out parameter, on success, should be false to indicate that
+     * id was not resolved; and true if id was resolved.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id,
+                        bool* resolved) {
+        Base* priv = Base::for_js(cx, obj);
+
+        if (!priv) {
+            // This catches a case in Object where the private struct isn't set
+            // until the initializer is called, so just defer to prototype
+            // chains in this case.
+            //
+            // This isn't too bad: either you get undefined if the field doesn't
+            // exist on any of the prototype chains, or whatever code will run
+            // afterwards will fail because of the "!priv" check there.
+            debug_jsprop_static("Resolve hook", id, obj);
+            *resolved = false;
+            return true;
+        }
+
+        priv->debug_jsprop("Resolve hook", id, obj);
+
+        if (!priv->is_prototype()) {
+            // We are an instance, not a prototype, so look for per-instance
+            // props that we want to define on the JSObject. Generally we do not
+            // want to cache these in JS, we want to always pull them from the C
+            // object, or JS would not see any changes made from C. So we use
+            // the property accessors, not this resolve hook.
+            *resolved = false;
+            return true;
+        }
+
+        const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+        if (id_is_never_lazy(id, atoms)) {
+            *resolved = false;
+            return true;
+        }
+
+        // A GObject-introspection lazy property will always be a string, so
+        // also bail out if trying to resolve an integer or symbol property.
+        JS::UniqueChars prop_name;
+        if (!gjs_get_string_id(cx, id, &prop_name))
+            return false;
+        if (!prop_name) {
+            *resolved = false;
+            return true;  // not resolved, but no error
+        }
+
+        return priv->to_prototype()->resolve_impl(cx, obj, id, prop_name.get(),
+                                                  resolved);
+    }
+
+    /*
+     * GIWrapperBase::finalize:
+     *
+     * This should always be included in the Base::klass vtable. The destructors
+     * of Prototype and Instance will be called in the finalize hook. It is not
+     * necessary to include a finalize_impl() function in Prototype or Instance.
+     * Any needed finalization should be done in ~Prototype() and ~Instance().
+     */
+    static void finalize(JSFreeOp* fop, JSObject* obj) {
+        Base* priv = Base::for_js_nocheck(obj);
+        g_assert(priv);
+
+        // Call only GIWrapperBase's original method here, not any overrides;
+        // e.g., we don't want to deal with a read barrier in ObjectInstance.
+        static_cast<GIWrapperBase*>(priv)->debug_lifecycle(obj, "Finalize");
+
+        if (priv->is_prototype())
+            priv->to_prototype()->finalize_impl(fop, obj);
+        else
+            priv->to_instance()->finalize_impl(fop, obj);
+
+        // Remove the pointer from the JSObject
+        JS_SetPrivate(obj, nullptr);
+    }
+
+    /*
+     * GIWrapperBase::trace:
+     *
+     * This should be included in the Base::klass vtable if any of the Base,
+     * Prototype or Instance structures contain any members that the JS garbage
+     * collector must trace. Each struct containing such members must override
+     * GIWrapperBase::trace_impl(), GIWrapperPrototype::trace_impl(), and/or
+     * GIWrapperInstance::trace_impl() in order to perform the trace.
+     */
+    static void trace(JSTracer* trc, JSObject* obj) {
+        Base* priv = Base::for_js_nocheck(obj);
+        if (!priv)
+            return;
+
+        // Don't log in trace(). That would overrun even the most verbose logs.
+
+        if (priv->is_prototype())
+            priv->to_prototype()->trace_impl(trc);
+        else
+            priv->to_instance()->trace_impl(trc);
+
+        priv->trace_impl(trc);
+    }
+
+    /*
+     * GIWrapperBase::trace_impl:
+     * Override if necessary. See trace().
+     */
+    void trace_impl(JSTracer* trc) {}
+
+    // JSNative methods
+
+    /*
+     * GIWrapperBase::constructor:
+     *
+     * C++ implementation of the JS constructor passed to JS_InitClass(). Only
+     * called on instances, never on prototypes. This method contains the
+     * functionality common to all GI wrapper classes. There must be a
+     * corresponding Instance::constructor_impl method containing the rest of
+     * the functionality.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp) {
+        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+        if (!args.isConstructing()) {
+            gjs_throw_constructor_error(cx);
+            return false;
+        }
+        JS::RootedObject obj(
+            cx, JS_NewObjectForConstructor(cx, &Base::klass, args));
+        if (!obj)
+            return false;
+
+        args.rval().setUndefined();
+
+        Instance* priv = Instance::new_for_js_object(cx, obj);
+
+        if (!priv->constructor_impl(cx, obj, args))
+            return false;
+
+        static_cast<GIWrapperBase*>(priv)->debug_lifecycle(obj,
+                                                           "JSObject created");
+        gjs_debug_lifecycle(Base::debug_topic, "m_proto is %p",
+                            priv->get_prototype());
+
+        // We may need to return a value different from obj (for example because
+        // we delegate to another constructor)
+        if (args.rval().isUndefined())
+            args.rval().setObject(*obj);
+        return true;
+    }
+
+    /*
+     * GIWrapperBase::to_string:
+     *
+     * JSNative method connected to the toString() method in JS.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool to_string(JSContext* cx, unsigned argc, JS::Value* vp) {
+        GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, Base, priv);
+        if (!priv)
+            return false;
+        return gjs_wrapper_to_string_func(
+            cx, obj, static_cast<const Base*>(priv)->to_string_kind(),
+            priv->info(), priv->gtype(), priv->ptr_addr(), args.rval());
+    }
+
+    // Helper methods
+
+ public:
+    /*
+     * GIWrapperBase::check_is_instance:
+     * @for_what: string used in the exception message if an exception is thrown
+     *
+     * Used in JSNative methods to ensure the passed-in JS object is an instance
+     * and not the prototype. Throws a JS exception if the prototype is passed
+     * in.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    bool check_is_instance(JSContext* cx, const char* for_what) const {
+        if (!is_prototype())
+            return true;
+        gjs_throw(cx, "Can't %s on %s.%s.prototype; only on instances",
+                  for_what, ns(), name());
+        return false;
+    }
+
+    // Public typecheck API
+
+    struct TypecheckNoThrow {};
+
+    /*
+     * GIWrapperBase::typecheck:
+     * @expected_info: (nullable): GI info to check
+     * @expected_type: (nullable): GType to check
+     *
+     * Checks not only that the JS object is of the correct JSClass (like
+     * for_js_typecheck() does); but also that the object is an instance, not
+     * the protptype; and that the instance's wrapped pointer is of the correct
+     * GType or GI info.
+     *
+     * The overload with a TypecheckNoThrow parameter will not throw a JS
+     * exception if the prototype is passed in or the typecheck fails.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool typecheck(JSContext* cx, JS::HandleObject object,
+                          GIBaseInfo* expected_info, GType expected_gtype) {
+        Base* priv = Base::for_js_typecheck(cx, object);
+        if (!priv || !priv->check_is_instance(cx, "convert to pointer"))
+            return false;
+
+        if (priv->to_instance()->typecheck_impl(cx, expected_info,
+                                                expected_gtype))
+            return true;
+
+        if (expected_info) {
+            gjs_throw_custom(
+                cx, JSProto_TypeError, nullptr,
+                "Object is of type %s.%s - cannot convert to %s.%s", priv->ns(),
+                priv->name(), g_base_info_get_namespace(expected_info),
+                g_base_info_get_name(expected_info));
+        } else {
+            gjs_throw_custom(cx, JSProto_TypeError, nullptr,
+                             "Object is of type %s.%s - cannot convert to %s",
+                             priv->ns(), priv->name(),
+                             g_type_name(expected_gtype));
+        }
+
+        return false;
+    }
+    GJS_USE
+    static bool typecheck(JSContext* cx, JS::HandleObject object,
+                          GIBaseInfo* expected_info, GType expected_gtype,
+                          TypecheckNoThrow) {
+        Base* priv = Base::for_js(cx, object);
+        if (!priv || priv->is_prototype())
+            return false;
+
+        return priv->to_instance()->typecheck_impl(cx, expected_info,
+                                                   expected_gtype);
+    }
+
+    // Deleting these constructors and assignment operators will also delete
+    // them from derived classes.
+    GIWrapperBase(const GIWrapperBase& other) = delete;
+    GIWrapperBase(GIWrapperBase&& other) = delete;
+    GIWrapperBase& operator=(const GIWrapperBase& other) = delete;
+    GIWrapperBase& operator=(GIWrapperBase&& other) = delete;
+};
+
+/*
+ * GIWrapperPrototype:
+ *
+ * The specialization of GIWrapperBase which becomes the private data of JS
+ * prototype objects. For example, it is the parent class of BoxedPrototype.
+ *
+ * Classes inheriting from GIWrapperPrototype must declare "friend class
+ * GIWrapperBase" as well as the normal CRTP requirement of "friend class
+ * GIWrapperPrototype", because of the unusual polymorphism scheme, in order for
+ * Base to call methods such as trace_impl().
+ */
+template <class Base, class Prototype, class Instance,
+          typename Info = GIObjectInfo>
+class GIWrapperPrototype : public Base {
+ protected:
+    // m_info may be null in the case of JS-defined types, although not all
+    // subclasses will allow this. Object and Interface allow it in any case.
+    // Use the method Base::is_custom_js_class() for clarity.
+    Info* m_info;
+    GType m_gtype;
+
+    explicit GIWrapperPrototype(Info* info, GType gtype)
+        : Base(),
+          m_info(info ? g_base_info_ref(info) : nullptr),
+          m_gtype(gtype) {
+        Base::debug_lifecycle("Prototype constructor");
+    }
+
+    ~GIWrapperPrototype(void) { g_clear_pointer(&m_info, g_base_info_unref); }
+
+    /*
+     * GIWrapperPrototype::init:
+     *
+     * Performs any initialization that cannot be done in the constructor of
+     * GIWrapperPrototype, either because it can fail, or because it can cause a
+     * garbage collection.
+     *
+     * This default implementation does nothing. Override in a subclass if
+     * necessary.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    bool init(JSContext* cx) { return true; }
+
+    // The following three methods are private because they are used only in
+    // define_class().
+
+ private:
+    /*
+     * GIWrapperPrototype::parent_proto:
+     *
+     * Returns in @proto the parent class's prototype object, or nullptr if
+     * there is none.
+     *
+     * This default implementation is for GObject introspection types that can't
+     * inherit in JS, like Boxed and Union. Override this if the type can
+     * inherit in JS.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    bool get_parent_proto(JSContext* cx, JS::MutableHandleObject proto) const {
+        proto.set(nullptr);
+        return true;
+    }
+
+    /*
+     * GIWrapperPrototype::constructor_nargs:
+     *
+     * Override this if the type's constructor takes other than 1 argument.
+     */
+    GJS_USE unsigned constructor_nargs(void) const { return 1; }
+
+    /*
+     * GIWrapperPrototype::define_jsclass:
+     * @in_object: JSObject on which to define the class constructor as a
+     *   property
+     * @parent_proto: (nullable): prototype of the prototype
+     * @constructor: return location for the constructor function object
+     * @prototype: return location for the prototype object
+     *
+     * Defines a JS class with constructor and prototype, and optionally defines
+     * properties and methods on the prototype object, and methods on the
+     * constructor object.
+     *
+     * By default no properties or methods are defined, but derived classes can
+     * override the GIWrapperBase::proto_properties,
+     * GIWrapperBase::proto_methods, and GIWrapperBase::static_methods members.
+     * Static properties would also be possible but are not used anywhere in GJS
+     * so are not implemented yet.
+     *
+     * Note: no prototype methods are defined if @parent_proto is null.
+     *
+     * Here is a refresher comment on the difference between __proto__ and
+     * prototype that has been in the GJS codebase since forever:
+     *
+     * 
https://web.archive.org/web/20100716231157/http://egachine.berlios.de/embedding-sm-best-practice/apa.html
+     * https://www.sitepoint.com/javascript-inheritance/
+     * http://www.cs.rit.edu/~atk/JavaScript/manuals/jsobj/
+     *
+     * What we want is: repoobj.Gtk.Window is constructor for a GtkWindow
+     * wrapper JSObject (gjs_define_object_class() is supposed to define Window
+     * in Gtk.)
+     *
+     * Window.prototype contains the methods on Window, e.g. set_default_size()
+     * mywindow.__proto__ is Window.prototype
+     * mywindow.__proto__.__proto__ is Bin.prototype
+     * mywindow.__proto__.__proto__.__proto__ is Container.prototype
+     *
+     * Because Window.prototype is an instance of Window in a sense,
+     * Window.prototype.__proto__ is Window.prototype, just as
+     * mywindow.__proto__ is Window.prototype
+     *
+     * If we do "mywindow = new Window()" then we should get:
+     *     mywindow.__proto__ == Window.prototype
+     * which means "mywindow instanceof Window" is true.
+     *
+     * Remember "Window.prototype" is "the __proto__ of stuff constructed with
+     * new Window()"
+     *
+     * __proto__ is used to search for properties if you do "this.foo", while
+     * .prototype is only relevant for constructors and is used to set __proto__
+     * on new'd objects. So .prototype only makes sense on constructors.
+     *
+     * JS_SetPrototype() and JS_GetPrototype() are for __proto__. To set/get
+     * .prototype, just use the normal property accessors, or JS_InitClass()
+     * sets it up automatically.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    bool define_jsclass(JSContext* cx, JS::HandleObject in_object,
+                        JS::HandleObject parent_proto,
+                        JS::MutableHandleObject constructor,
+                        JS::MutableHandleObject prototype) {
+        // The GI namespace is only used to set the JSClass->name field (exposed
+        // by Object.prototype.toString, for example). We can safely set
+        // "unknown" if this is a custom JS class with no GI namespace, as in
+        // that case the name is already globally unique (it's a GType name).
+        const char* gi_namespace =
+            Base::is_custom_js_class() ? "unknown" : Base::ns();
+
+        unsigned nargs = static_cast<Prototype*>(this)->constructor_nargs();
+
+        if (!gjs_init_class_dynamic(
+                cx, in_object, parent_proto, gi_namespace, Base::name(),
+                &Base::klass, &Base::constructor, nargs, Base::proto_properties,
+                parent_proto ? nullptr : Base::proto_methods,
+                nullptr,  // static properties, MyClass.myprop; not yet needed
+                Base::static_methods, prototype, constructor))
+            return false;
+
+        gjs_debug(Base::debug_topic,
+                  "Defined class for %s (%s), prototype %p, "
+                  "JSClass %p, in object %p",
+                  Base::name(), Base::type_name(), prototype.get(),
+                  JS_GetClass(prototype), in_object.get());
+
+        return true;
+    }
+
+ public:
+    /**
+     * GIWrapperPrototype::create_class:
+     * @in_object: JSObject on which to define the class constructor as a
+     *   property
+     * @info: (nullable): Introspection info for the class, or null if the class
+     *   has been defined in JS
+     * @gtype: GType for the class
+     * @constructor: return location for the constructor function object
+     * @prototype: return location for the prototype object
+     *
+     * Creates a JS class that wraps a GI pointer, by defining its constructor
+     * function and prototype object. The prototype object is given an instance
+     * of GIWrapperPrototype as its private data, which is also returned.
+     * Basically treat this method as the public constructor.
+     *
+     * Also defines all the requested methods and properties on the prototype
+     * and constructor objects (see define_jsclass()), as well as a `$gtype`
+     * property and a toString() method.
+     *
+     * This method can be overridden and chained up to if the derived class
+     * needs to define more properties on the constructor or prototype objects,
+     * e.g. eager GI properties.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static Prototype* create_class(JSContext* cx, JS::HandleObject in_object,
+                                   Info* info, GType gtype,
+                                   JS::MutableHandleObject constructor,
+                                   JS::MutableHandleObject prototype) {
+        g_assert(in_object);
+        g_assert(gtype != G_TYPE_INVALID);
+
+        auto* priv = g_slice_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) ||
+            !priv->define_jsclass(cx, in_object, parent_proto, constructor,
+                                  prototype) ||
+            !gjs_wrapper_define_gtype_prop(cx, constructor, gtype))
+            return nullptr;
+
+        // Every class has a toString() with C++ implementation, so define that
+        // without requiring it to be listed in Base::proto_methods
+        if (!parent_proto) {
+            const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+            if (!JS_DefineFunctionById(cx, prototype, atoms.to_string(),
+                                       &Base::to_string, 0,
+                                       GJS_MODULE_PROP_FLAGS))
+                return nullptr;
+        }
+
+        JS_SetPrivate(prototype, priv);
+        return priv;
+    }
+
+    // Methods to get an existing Prototype
+
+    /*
+     * GIWrapperPrototype::for_js:
+     *
+     * Like Base::for_js(), but asserts that the returned private struct is a
+     * Prototype and not an Instance.
+     */
+    GJS_USE static Prototype* for_js(JSContext* cx, JS::HandleObject wrapper) {
+        return Base::for_js(cx, wrapper)->to_prototype();
+    }
+
+    /*
+     * GIWrapperPrototype::for_js_prototype:
+     *
+     * Gets the Prototype private data from to @wrapper.prototype. Cannot return
+     * null, and asserts so.
+     */
+    GJS_USE
+    static Prototype* for_js_prototype(JSContext* cx,
+                                       JS::HandleObject wrapper) {
+        JS::RootedObject proto(cx);
+        JS_GetPrototype(cx, wrapper, &proto);
+        Base* retval = Base::for_js(cx, proto);
+        g_assert(retval);
+        return retval->to_prototype();
+    }
+
+    // Accessors
+
+    GJS_USE Info* info(void) const { return m_info; }
+    GJS_USE GType gtype(void) const { return m_gtype; }
+
+    // JSClass operations
+
+ protected:
+    void finalize_impl(JSFreeOp* fop, JSObject* obj) {
+        static_cast<Prototype*>(this)->~Prototype();
+        g_slice_free(Prototype, this);
+    }
+
+    // Override if necessary
+    void trace_impl(JSTracer* trc) {}
+};
+
+/*
+ * GIWrapperInstance:
+ *
+ * The specialization of GIWrapperBase which becomes the private data of JS
+ * instance objects. For example, it is the parent class of BoxedInstance.
+ *
+ * Classes inheriting from GIWrapperInstance must declare "friend class
+ * GIWrapperBase" as well as the normal CRTP requirement of "friend class
+ * GIWrapperInstance", because of the unusual polymorphism scheme, in order for
+ * Base to call methods such as trace_impl().
+ */
+template <class Base, class Prototype, class Instance, typename Wrapped = void>
+class GIWrapperInstance : public Base {
+ protected:
+    Wrapped* m_ptr;
+
+    explicit GIWrapperInstance(JSContext* cx, JS::HandleObject obj)
+        : Base(Prototype::for_js_prototype(cx, obj)) {
+        Base::GIWrapperBase::debug_lifecycle(obj, "Instance constructor");
+    }
+    ~GIWrapperInstance(void) {}
+
+ public:
+    /*
+     * GIWrapperInstance::new_for_js_object:
+     *
+     * Creates a GIWrapperInstance and associates it with @obj as its private
+     * data. This is called by the JS constructor. Uses the slice allocator.
+     */
+    GJS_USE
+    static Instance* new_for_js_object(JSContext* cx, JS::HandleObject obj) {
+        g_assert(!JS_GetPrivate(obj));
+        auto* priv = g_slice_new0(Instance);
+        new (priv) Instance(cx, obj);
+
+        // Init the private variable before we do anything else. If a garbage
+        // collection happens when calling the constructor, then this object
+        // might be traced and we would end up dereferencing a null pointer.
+        JS_SetPrivate(obj, priv);
+
+        return priv;
+    }
+
+    // Method to get an existing Instance
+
+    /*
+     * GIWrapperInstance::for_js:
+     *
+     * Like Base::for_js(), but asserts that the returned private struct is an
+     * Instance and not a Prototype.
+     */
+    GJS_USE static Instance* for_js(JSContext* cx, JS::HandleObject wrapper) {
+        return Base::for_js(cx, wrapper)->to_instance();
+    }
+
+    // Accessors
+
+    GJS_USE Wrapped* ptr(void) const { return m_ptr; }
+    /*
+     * GIWrapperInstance::raw_ptr:
+     *
+     * Like ptr(), but returns a byte pointer for use in byte arithmetic.
+     */
+    GJS_USE uint8_t* raw_ptr(void) const {
+        return reinterpret_cast<uint8_t*>(m_ptr);
+    }
+
+    // JSClass operations
+
+ protected:
+    void finalize_impl(JSFreeOp* fop, JSObject* obj) {
+        static_cast<Instance*>(this)->~Instance();
+        g_slice_free(Instance, this);
+    }
+
+    // Override if necessary
+    void trace_impl(JSTracer* trc) {}
+
+    // Helper methods
+
+    /*
+     * GIWrapperInstance::typecheck_impl:
+     *
+     * See GIWrapperBase::typecheck(). Checks that the instance's wrapped
+     * pointer is of the correct GType or GI info. Does not throw a JS
+     * exception.
+     *
+     * It's possible to override typecheck_impl() if you need an extra step in
+     * the check.
+     */
+    GJS_USE
+    bool typecheck_impl(JSContext* cx, GIBaseInfo* expected_info,
+                        GType expected_gtype) const {
+        if (expected_gtype != G_TYPE_NONE)
+            return g_type_is_a(Base::gtype(), expected_gtype);
+        else if (expected_info)
+            return g_base_info_equal(Base::info(), expected_info);
+        return true;
+    }
+};
+
 #endif  // GI_WRAPPERUTILS_H_



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