/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2008 litl, LLC
// SPDX-FileCopyrightText: 2018 Philip Chimento <philip.chimento@gmail.com>

#include <config.h>

#include <stdint.h>
#include <string.h>  // for memset, strcmp

#include <algorithm>  // for find
#include <functional>  // for mem_fn
#include <limits>
#include <string>
#include <tuple>  // for tie
#include <unordered_set>
#include <utility>      // for move
#include <vector>

#include <girepository.h>
#include <glib-object.h>
#include <glib.h>

#include <js/CallAndConstruct.h>  // for IsCallable, JS_CallFunctionValue
#include <js/CallArgs.h>
#include <js/CharacterEncoding.h>
#include <js/Class.h>
#include <js/ComparisonOperators.h>
#include <js/ErrorReport.h>         // for JS_ReportOutOfMemory
#include <js/GCAPI.h>               // for JS_AddWeakPointerCompartmentCallback
#include <js/GCVector.h>            // for MutableWrappedPtrOperations
#include <js/HeapAPI.h>
#include <js/MemoryFunctions.h>     // for AddAssociatedMemory, RemoveAssoci...
#include <js/PropertyAndElement.h>
#include <js/PropertyDescriptor.h>  // for JSPROP_PERMANENT, JSPROP_READONLY
#include <js/String.h>
#include <js/Symbol.h>
#include <js/TypeDecls.h>
#include <js/Utility.h>  // for UniqueChars
#include <js/Value.h>
#include <js/ValueArray.h>
#include <js/Warnings.h>
#include <jsapi.h>        // for JS_GetFunctionObject, IdVector
#include <jsfriendapi.h>  // for JS_GetObjectFunction, GetFunctionNativeReserved
#include <mozilla/HashTable.h>

#include "gi/arg-inl.h"
#include "gi/arg.h"
#include "gi/closure.h"
#include "gi/cwrapper.h"
#include "gi/function.h"
#include "gi/gjs_gi_trace.h"
#include "gi/object.h"
#include "gi/repo.h"
#include "gi/toggle.h"
#include "gi/utils-inl.h"  // for gjs_int_to_pointer
#include "gi/value.h"
#include "gi/wrapperutils.h"
#include "cjs/atoms.h"
#include "cjs/context-private.h"
#include "cjs/deprecation.h"
#include "cjs/jsapi-class.h"
#include "cjs/jsapi-util.h"
#include "cjs/jsapi-util-args.h"
#include "cjs/jsapi-util-root.h"
#include "cjs/macros.h"
#include "cjs/mem-private.h"
#include "cjs/profiler-private.h"
#include "util/log.h"

class JSTracer;

/* This is a trick to print out the sizes of the structs at compile time, in
 * an error message. */
// template <int s> struct Measure;
// Measure<sizeof(ObjectInstance)> instance_size;
// Measure<sizeof(ObjectPrototype)> prototype_size;

#if defined(__x86_64__) && defined(__clang__)
/* This isn't meant to be comprehensive, but should trip on at least one CI job
 * if sizeof(ObjectInstance) is increased. */
static_assert(sizeof(ObjectInstance) <= 64,
              "Think very hard before increasing the size of ObjectInstance. "
              "There can be tens of thousands of them alive in a typical "
              "gnome-shell run.");
#endif  // x86-64 clang

bool ObjectInstance::s_weak_pointer_callback = false;
decltype(ObjectInstance::s_wrapped_gobject_list)
    ObjectInstance::s_wrapped_gobject_list;

static const auto DISPOSED_OBJECT = std::numeric_limits<uintptr_t>::max();

GJS_JSAPI_RETURN_CONVENTION
static JSObject* gjs_lookup_object_prototype_from_info(JSContext*,
                                                       GIObjectInfo*, GType);

// clang-format off
G_DEFINE_QUARK(gjs::custom-type, ObjectBase::custom_type)
G_DEFINE_QUARK(gjs::custom-property, ObjectBase::custom_property)
G_DEFINE_QUARK(gjs::instance-strings, ObjectBase::instance_strings)
G_DEFINE_QUARK(gjs::disposed, ObjectBase::disposed)
// clang-format on

[[nodiscard]] static GQuark gjs_object_priv_quark() {
    static GQuark val = 0;
    if (G_UNLIKELY (!val))
        val = g_quark_from_static_string ("gjs::private");

    return val;
}

bool ObjectBase::is_custom_js_class() {
    return !!g_type_get_qdata(gtype(), ObjectBase::custom_type_quark());
}

// Plain g_type_query fails and leaves @query uninitialized for dynamic types.
// See https://gitlab.gnome.org/GNOME/glib/issues/623
void ObjectBase::type_query_dynamic_safe(GTypeQuery* query) {
    GType type = gtype();
    while (g_type_get_qdata(type, ObjectBase::custom_type_quark()))
        type = g_type_parent(type);

    g_type_query(type, query);
}

void ObjectInstance::link() {
    g_assert(std::find(s_wrapped_gobject_list.begin(),
                       s_wrapped_gobject_list.end(),
                       this) == s_wrapped_gobject_list.end());
    s_wrapped_gobject_list.push_back(this);
}

void ObjectInstance::unlink() {
    Gjs::remove_one_from_unsorted_vector(&s_wrapped_gobject_list, this);
}

const void* ObjectBase::jsobj_addr(void) const {
    if (is_prototype())
        return nullptr;
    return to_instance()->m_wrapper.debug_addr();
}

// Overrides GIWrapperBase::typecheck(). We only override the overload that
// throws, so that we can throw our own more informative error.
bool ObjectBase::typecheck(JSContext* cx, JS::HandleObject obj,
                           GIObjectInfo* expected_info, GType expected_gtype) {
    if (GIWrapperBase::typecheck(cx, obj, expected_info, expected_gtype))
        return true;

    gjs_throw(cx,
              "This JS object wrapper isn't wrapping a GObject."
              " If this is a custom subclass, are you sure you chained"
              " up to the parent _init properly?");
    return false;
}

bool ObjectInstance::check_gobject_disposed_or_finalized(
    const char* for_what) const {
    if (!m_gobj_disposed)
        return true;

    g_critical(
        "Object %s.%s (%p), has been already %s — impossible to %s "
        "it. This might be caused by the object having been destroyed from C "
        "code using something such as destroy(), dispose(), or remove() "
        "vfuncs.\n%s",
        ns(), name(), m_ptr.get(), m_gobj_finalized ? "finalized" : "disposed",
        for_what, gjs_dumpstack_string().c_str());
    return false;
}

bool ObjectInstance::check_gobject_finalized(const char* for_what) const {
    if (check_gobject_disposed_or_finalized(for_what))
        return true;

    return !m_gobj_finalized;
}

ObjectInstance *
ObjectInstance::for_gobject(GObject *gobj)
{
    auto priv = static_cast<ObjectInstance *>(g_object_get_qdata(gobj,
                                                                 gjs_object_priv_quark()));

    if (priv)
        priv->check_js_object_finalized();

    return priv;
}

void
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_ptr.get(), type_name());
        m_wrapper_finalized = false;
        g_assert(!m_wrapper);  /* should associate again with a new wrapper */
    }
}

ObjectPrototype* ObjectPrototype::for_gtype(GType gtype) {
    return static_cast<ObjectPrototype*>(
        g_type_get_qdata(gtype, gjs_object_priv_quark()));
}

void ObjectPrototype::set_type_qdata(void) {
    g_type_set_qdata(m_gtype, gjs_object_priv_quark(), this);
}

void
ObjectInstance::set_object_qdata(void)
{
    g_object_set_qdata_full(
        m_ptr, gjs_object_priv_quark(), this, [](void* object) {
            auto* self = static_cast<ObjectInstance*>(object);
            if (G_UNLIKELY(!self->m_gobj_disposed)) {
                g_warning(
                    "Object %p (a %s) was finalized but we didn't track "
                    "its disposal",
                    self->m_ptr.get(), g_type_name(self->gtype()));
                self->m_gobj_disposed = true;
            }
            self->m_gobj_finalized = true;
            gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
                                "Wrapped GObject %p finalized",
                                self->m_ptr.get());
        });
}

void
ObjectInstance::unset_object_qdata(void)
{
    auto priv_quark = gjs_object_priv_quark();
    if (g_object_get_qdata(m_ptr, priv_quark) == this)
        g_object_steal_qdata(m_ptr, priv_quark);
}

GParamSpec* ObjectPrototype::find_param_spec_from_id(JSContext* cx,
                                                     JS::HandleString key) {
    /* First check for the ID in the cache */
    auto entry = m_property_cache.lookupForAdd(key);
    if (entry)
        return entry->value();

    JS::UniqueChars js_prop_name(JS_EncodeStringToUTF8(cx, key));
    if (!js_prop_name)
        return nullptr;

    GjsAutoChar gname = gjs_hyphen_from_camel(js_prop_name.get());
    GjsAutoTypeClass<GObjectClass> gobj_class(m_gtype);
    GParamSpec* pspec = g_object_class_find_property(gobj_class, gname);
    GjsAutoParam param_spec(pspec, GjsAutoTakeOwnership());

    if (!param_spec) {
        gjs_wrapper_throw_nonexistent_field(cx, m_gtype, js_prop_name.get());
        return nullptr;
    }

    if (!m_property_cache.add(entry, key, std::move(param_spec))) {
        JS_ReportOutOfMemory(cx);
        return nullptr;
    }
    return pspec; /* owned by property cache */
}

/* 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
 * custom state is set on it, because we need to keep the JS GObject wrapper
 * alive in order not to lose custom "expando" properties.
 */
bool ObjectBase::add_property(JSContext* cx, JS::HandleObject obj,
                              JS::HandleId id, JS::HandleValue value) {
    auto* priv = ObjectBase::for_js(cx, obj);

    /* priv is null during init: property is not being added from JS */
    if (!priv) {
        debug_jsprop_static("Add property hook", id, obj);
        return true;
    }
    if (priv->is_prototype())
        return true;

    return priv->to_instance()->add_property_impl(cx, obj, id, value);
}

bool ObjectInstance::add_property_impl(JSContext* cx, JS::HandleObject obj,
                                       JS::HandleId id, JS::HandleValue) {
    debug_jsprop("Add property hook", id, obj);

    if (is_custom_js_class())
        return true;

    if (!ensure_uses_toggle_ref(cx)) {
        gjs_throw(cx, "Impossible to set toggle references on %sobject %p",
                  m_gobj_disposed ? "disposed " : "", m_ptr.get());
        return false;
    }

    return true;
}

bool ObjectBase::prop_getter(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);

    JS::RootedString name(cx,
        gjs_dynamic_property_private_slot(&args.callee()).toString());

    std::string fullName{priv->format_name() + "[" + gjs_debug_string(name) +
                         "]"};
    AutoProfilerLabel label(cx, "property getter", fullName.c_str());

    priv->debug_jsprop("Property getter", name, obj);

    if (priv->is_prototype())
        return true;
        /* Ignore silently; note that this is different from what we do for
         * boxed types, for historical reasons */

    return priv->to_instance()->prop_getter_impl(cx, name, args.rval());
}

bool ObjectInstance::prop_getter_impl(JSContext* cx, JS::HandleString name,
                                      JS::MutableHandleValue rval) {
    if (!check_gobject_finalized("get any property from")) {
        rval.setUndefined();
        return true;
    }

    ObjectPrototype* proto_priv = get_prototype();
    GParamSpec *param = proto_priv->find_param_spec_from_id(cx, name);

    /* This is guaranteed because we resolved the property before */
    g_assert(param);

    /* Do not fetch JS overridden properties from GObject, to avoid
     * infinite recursion. */
    if (g_param_spec_get_qdata(param, ObjectBase::custom_property_quark()))
        return true;

    if ((param->flags & G_PARAM_READABLE) == 0) {
        rval.setUndefined();
        return true;
    }

    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Accessing GObject property %s",
                     param->name);

    Gjs::AutoGValue gvalue(G_PARAM_SPEC_VALUE_TYPE(param));
    g_object_get_property(m_ptr, param->name, &gvalue);

    return gjs_value_from_g_value(cx, rval, &gvalue);
}

[[nodiscard]] static GjsAutoFieldInfo lookup_field_info(GIObjectInfo* info,
                                                        const char* name) {
    int n_fields = g_object_info_get_n_fields(info);
    int ix;
    GjsAutoFieldInfo retval;

    for (ix = 0; ix < n_fields; ix++) {
        retval = g_object_info_get_field(info, ix);
        if (strcmp(name, retval.name()) == 0)
            break;
        retval.reset();
    }

    if (!retval || !(g_field_info_get_flags(retval) & GI_FIELD_IS_READABLE))
        return nullptr;

    return retval;
}

bool ObjectBase::field_getter(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);

    JS::RootedString name(cx,
        gjs_dynamic_property_private_slot(&args.callee()).toString());

    std::string fullName = priv->format_name() + "." + gjs_debug_string(name);
    AutoProfilerLabel label(cx, "field getter", fullName.c_str());

    priv->debug_jsprop("Field getter", name, obj);

    if (priv->is_prototype())
        return true;
        /* Ignore silently; note that this is different from what we do for
         * boxed types, for historical reasons */

    return priv->to_instance()->field_getter_impl(cx, name, args.rval());
}

bool ObjectInstance::field_getter_impl(JSContext* cx, JS::HandleString name,
                                       JS::MutableHandleValue rval) {
    if (!check_gobject_finalized("get any property from"))
        return true;

    ObjectPrototype* proto_priv = get_prototype();
    GIFieldInfo* field = proto_priv->lookup_cached_field_info(cx, name);
    GITypeTag tag;
    GIArgument arg = { 0 };

    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Overriding %s with GObject field",
                     gjs_debug_string(name).c_str());

    GjsAutoTypeInfo type = g_field_info_get_type(field);
    tag = g_type_info_get_tag(type);

    switch (tag) {
        case GI_TYPE_TAG_ARRAY:
        case GI_TYPE_TAG_ERROR:
        case GI_TYPE_TAG_GHASH:
        case GI_TYPE_TAG_GLIST:
        case GI_TYPE_TAG_GSLIST:
        case GI_TYPE_TAG_INTERFACE:
            gjs_throw(cx,
                      "Can't get field %s; GObject introspection supports only "
                      "fields with simple types, not %s",
                      gjs_debug_string(name).c_str(),
                      g_type_tag_to_string(tag));
            return false;

        default:
            break;
    }

    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;
    }

    return gjs_value_from_g_argument(cx, rval, type, GJS_ARGUMENT_FIELD,
                                     GI_TRANSFER_EVERYTHING, &arg);
    /* transfer is irrelevant because g_field_info_get_field() doesn't
     * handle boxed types */
}

/* 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_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);

    JS::RootedString name(cx,
        gjs_dynamic_property_private_slot(&args.callee()).toString());

    std::string fullName{priv->format_name() + "[" + gjs_debug_string(name) +
                         "]"};
    AutoProfilerLabel label(cx, "property setter", fullName.c_str());

    priv->debug_jsprop("Property setter", name, obj);

    if (priv->is_prototype())
        return true;
        /* Ignore silently; note that this is different from what we do for
         * boxed types, for historical reasons */

    /* Clear the JS stored value, to avoid keeping additional references */
    args.rval().setUndefined();

    return priv->to_instance()->prop_setter_impl(cx, name, args[0]);
}

bool ObjectInstance::prop_setter_impl(JSContext* cx, JS::HandleString name,
                                      JS::HandleValue value) {
    if (!check_gobject_finalized("set any property on"))
        return true;

    ObjectPrototype* proto_priv = get_prototype();
    GParamSpec *param_spec = proto_priv->find_param_spec_from_id(cx, name);
    if (!param_spec)
        return false;

    /* Do not set JS overridden properties through GObject, to avoid
     * infinite recursion (unless constructing) */
    if (g_param_spec_get_qdata(param_spec, ObjectBase::custom_property_quark()))
        return true;

    if (!(param_spec->flags & G_PARAM_WRITABLE))
        /* prevent setting the prop even in JS */
        return gjs_wrapper_throw_readonly_field(cx, gtype(), param_spec->name);

    if (param_spec->flags & G_PARAM_DEPRECATED) {
        const std::string& class_name = format_name();
        _gjs_warn_deprecated_once_per_callsite(
            cx, DeprecatedGObjectProperty,
            {class_name.c_str(), param_spec->name});
    }

    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Setting GObject prop %s",
                     param_spec->name);

    Gjs::AutoGValue gvalue(G_PARAM_SPEC_VALUE_TYPE(param_spec));
    if (!gjs_value_to_g_value(cx, value, &gvalue))
        return false;

    g_object_set_property(m_ptr, param_spec->name, &gvalue);

    return true;
}

bool ObjectBase::field_setter(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);

    JS::RootedString name(cx,
        gjs_dynamic_property_private_slot(&args.callee()).toString());

    std::string fullName{priv->format_name() + "[" + gjs_debug_string(name) +
                         "]"};
    AutoProfilerLabel label(cx, "field setter", fullName.c_str());

    priv->debug_jsprop("Field setter", name, obj);

    if (priv->is_prototype())
        return true;
        /* Ignore silently; note that this is different from what we do for
         * boxed types, for historical reasons */

    /* We have to update args.rval(), because JS caches it as the property's "stored
     * value" (https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/JSAPI_reference/Stored_value)
     * and so subsequent gets would get the stored value instead of accessing
     * the field */
    args.rval().setUndefined();

    return priv->to_instance()->field_setter_not_impl(cx, name);
}

bool ObjectInstance::field_setter_not_impl(JSContext* cx,
                                           JS::HandleString name) {
    if (!check_gobject_finalized("set GObject field on"))
        return true;

    ObjectPrototype* proto_priv = get_prototype();
    GIFieldInfo* field = proto_priv->lookup_cached_field_info(cx, name);

    /* As far as I know, GI never exposes GObject instance struct fields as
     * writable, so no need to implement this for the time being */
    if (g_field_info_get_flags(field) & GI_FIELD_IS_WRITABLE) {
        g_message("Field %s of a GObject is writable, but setting it is not "
                  "implemented", gjs_debug_string(name).c_str());
        return true;
    }

    return gjs_wrapper_throw_readonly_field(cx, gtype(),
                                            g_base_info_get_name(field));
}

bool ObjectPrototype::is_vfunc_unchanged(GIVFuncInfo* info) {
    GjsAutoError error;
    GType ptype = g_type_parent(m_gtype);
    gpointer addr1, addr2;

    addr1 = g_vfunc_info_get_address(info, m_gtype, &error);
    if (error)
        return false;

    addr2 = g_vfunc_info_get_address(info, ptype, &error);
    if (error)
        return false;

    return addr1 == addr2;
}

[[nodiscard]] static GjsAutoVFuncInfo find_vfunc_on_parents(
    GIObjectInfo* info, const char* name, bool* out_defined_by_parent) {
    bool defined_by_parent = false;

    /* ref the first info so that we don't destroy
     * it when unrefing parents later */
    GjsAutoObjectInfo parent(info, GjsAutoTakeOwnership());

    /* Since it isn't possible to override a vfunc on
     * an interface without reimplementing it, we don't need
     * to search the parent types when looking for a vfunc. */
    GjsAutoVFuncInfo vfunc =
        g_object_info_find_vfunc_using_interfaces(parent, name, nullptr);
    while (!vfunc && parent) {
        parent = g_object_info_get_parent(parent);
        if (parent)
            vfunc = g_object_info_find_vfunc(parent, name);

        defined_by_parent = true;
    }

    if (out_defined_by_parent)
        *out_defined_by_parent = defined_by_parent;

    return vfunc;
}

/* Taken from GLib */
static void canonicalize_key(const GjsAutoChar& key) {
    for (char* p = key; *p != 0; p++) {
        char c = *p;

        if (c != '-' && (c < '0' || c > '9') && (c < 'A' || c > 'Z') &&
            (c < 'a' || c > 'z'))
            *p = '-';
    }
}

/* @name must already be canonicalized */
[[nodiscard]] static bool is_ginterface_property_name(GIInterfaceInfo* info,
                                                      const char* name) {
    int n_props = g_interface_info_get_n_properties(info);
    GjsAutoPropertyInfo prop_info;

    for (int ix = 0; ix < n_props; ix++) {
        prop_info = g_interface_info_get_property(info, ix);
        if (strcmp(name, prop_info.name()) == 0)
            break;
        prop_info.reset();
    }

    return !!prop_info;
}

bool ObjectPrototype::lazy_define_gobject_property(JSContext* cx,
                                                   JS::HandleObject obj,
                                                   JS::HandleId id,
                                                   bool* resolved,
                                                   const char* name) {
    bool found = false;
    if (!JS_AlreadyHasOwnPropertyById(cx, obj, id, &found))
        return false;
    if (found) {
        /* Already defined, so *resolved = false because we didn't just
         * define it */
        *resolved = false;
        return true;
    }

    debug_jsprop("Defining lazy GObject property", id, obj);

    JS::RootedValue private_id(cx, JS::StringValue(id.toString()));
    if (!gjs_define_property_dynamic(
            cx, obj, name, "gobject_prop", &ObjectBase::prop_getter,
            &ObjectBase::prop_setter, private_id,
            // Make property configurable so that interface properties can be
            // overridden by GObject.ParamSpec.override in the class that
            // implements them
            GJS_MODULE_PROP_FLAGS & ~JSPROP_PERMANENT))
        return false;

    *resolved = true;
    return true;
}

// An object shared by the getter and setter to store the interface' prototype
// and overrides.
static constexpr size_t ACCESSOR_SLOT = 0;

static bool interface_getter(JSContext* cx, unsigned argc, JS::Value* vp) {
    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);

    JS::RootedValue v_accessor(
        cx, js::GetFunctionNativeReserved(&args.callee(), ACCESSOR_SLOT));
    g_assert(v_accessor.isObject() && "accessor must be an object");
    JS::RootedObject accessor(cx, &v_accessor.toObject());

    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);

    // Check if an override value has been set
    bool has_override_symbol = false;
    if (!JS_HasPropertyById(cx, accessor, atoms.override(),
                            &has_override_symbol))
        return false;

    if (has_override_symbol) {
        JS::RootedValue v_override_symbol(cx);
        if (!JS_GetPropertyById(cx, accessor, atoms.override(),
                                &v_override_symbol))
            return false;
        g_assert(v_override_symbol.isSymbol() &&
                 "override symbol must be a symbol");
        JS::RootedSymbol override_symbol(cx, v_override_symbol.toSymbol());
        JS::RootedId override_id(cx, JS::PropertyKey::Symbol(override_symbol));

        JS::RootedObject this_obj(cx);
        if (!args.computeThis(cx, &this_obj))
            return false;

        bool has_override = false;
        if (!JS_HasPropertyById(cx, this_obj, override_id, &has_override))
            return false;

        if (has_override)
            return JS_GetPropertyById(cx, this_obj, override_id, args.rval());
    }

    JS::RootedValue v_prototype(cx);
    if (!JS_GetPropertyById(cx, accessor, atoms.prototype(), &v_prototype))
        return false;
    g_assert(v_prototype.isObject() && "prototype must be an object");

    JS::RootedObject prototype(cx, &v_prototype.toObject());
    JS::RootedId id(cx, JS::PropertyKey::NonIntAtom(JS_GetFunctionId(
                            JS_GetObjectFunction(&args.callee()))));
    return JS_GetPropertyById(cx, prototype, id, args.rval());
}

static bool interface_setter(JSContext* cx, unsigned argc, JS::Value* vp) {
    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
    JS::RootedValue v_accessor(
        cx, js::GetFunctionNativeReserved(&args.callee(), ACCESSOR_SLOT));
    JS::RootedObject accessor(cx, &v_accessor.toObject());
    JS::RootedString description(
        cx, JS_AtomizeAndPinString(cx, "Private interface function setter"));
    JS::RootedSymbol symbol(cx, JS::NewSymbol(cx, description));
    JS::RootedValue v_symbol(cx, JS::SymbolValue(symbol));

    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
    if (!JS_SetPropertyById(cx, accessor, atoms.override(), v_symbol))
        return false;

    args.rval().setUndefined();

    JS::RootedObject this_obj(cx);
    if (!args.computeThis(cx, &this_obj))
        return false;
    JS::RootedId override_id(cx, JS::PropertyKey::Symbol(symbol));

    return JS_SetPropertyById(cx, this_obj, override_id, args[0]);
}

static bool resolve_on_interface_prototype(JSContext* cx,
                                           GIObjectInfo* iface_info,
                                           JS::HandleId identifier,
                                           JS::HandleObject class_prototype,
                                           bool* found) {
    GType gtype = g_base_info_get_type(iface_info);
    JS::RootedObject interface_prototype(
        cx, gjs_lookup_object_prototype_from_info(cx, iface_info, gtype));
    if (!interface_prototype)
        return false;

    bool exists = false;
    if (!JS_HasPropertyById(cx, interface_prototype, identifier, &exists))
        return false;

    // If the property doesn't exist on the interface prototype, we don't need
    // to perform this trick.
    if (!exists) {
        *found = false;
        return true;
    }

    // Lazily define a property on the class prototype if a property
    // of that name is present on an interface prototype that the class
    // implements.
    //
    // Define a property of the same name on the class prototype, with a
    // getter and setter. This is so that e.g. file.dup() calls the _current_
    // value of Gio.File.prototype.dup(), not the original, so that it can be
    // overridden (or monkeypatched).
    //
    // The setter (interface_setter() above) marks the property as overridden if
    // it is set from user code. The getter (interface_getter() above) proxies
    // the interface prototype's property, unless it was marked as overridden.
    //
    // Store the identifier in the getter and setter function's ID slots for
    // to enable looking up the original value on the interface prototype.
    JS::RootedObject getter(
        cx, JS_GetFunctionObject(js::NewFunctionByIdWithReserved(
                cx, interface_getter, 0, 0, identifier)));
    if (!getter)
        return false;

    JS::RootedObject setter(
        cx, JS_GetFunctionObject(js::NewFunctionByIdWithReserved(
                cx, interface_setter, 1, 0, identifier)));
    if (!setter)
        return false;

    JS::RootedObject accessor(cx, JS_NewPlainObject(cx));
    if (!accessor)
        return false;

    js::SetFunctionNativeReserved(setter, ACCESSOR_SLOT,
                                  JS::ObjectValue(*accessor));
    js::SetFunctionNativeReserved(getter, ACCESSOR_SLOT,
                                  JS::ObjectValue(*accessor));

    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
    JS::RootedValue v_prototype(cx, JS::ObjectValue(*interface_prototype));
    if (!JS_SetPropertyById(cx, accessor, atoms.prototype(), v_prototype))
        return false;

    // Create a new descriptor with our getter and setter, that is configurable
    // and enumerable, because GObject may need to redefine it later.
    JS::PropertyAttributes attrs{JS::PropertyAttribute::Configurable,
                                 JS::PropertyAttribute::Enumerable};
    JS::Rooted<JS::PropertyDescriptor> desc(
        cx, JS::PropertyDescriptor::Accessor(getter, setter, attrs));

    if (!JS_DefinePropertyById(cx, class_prototype, identifier, desc))
        return false;

    *found = true;
    return true;
}

bool ObjectPrototype::resolve_no_info(JSContext* cx, JS::HandleObject obj,
                                      JS::HandleId id, bool* resolved,
                                      const char* name,
                                      ResolveWhat resolve_props) {
    guint n_interfaces;
    guint i;

    GjsAutoChar canonical_name;
    if (resolve_props == ConsiderMethodsAndProperties) {
        // Optimization: GObject property names must start with a letter
        if (g_ascii_isalpha(name[0])) {
            canonical_name = gjs_hyphen_from_camel(name);
            canonicalize_key(canonical_name);
        }
    }

    GIInterfaceInfo** interfaces;
    g_irepository_get_object_gtype_interfaces(nullptr, m_gtype, &n_interfaces,
        &interfaces);

    /* Fallback to GType system for non custom GObjects with no GI information
     */
    if (canonical_name && G_TYPE_IS_CLASSED(m_gtype) && !is_custom_js_class()) {
        GjsAutoTypeClass<GObjectClass> oclass(m_gtype);

        if (g_object_class_find_property(oclass, canonical_name))
            return lazy_define_gobject_property(cx, obj, id, resolved, name);

        for (i = 0; i < n_interfaces; i++) {
            GType iface_gtype =
                g_registered_type_info_get_g_type(interfaces[i]);
            if (!G_TYPE_IS_CLASSED(iface_gtype))
                continue;

            GjsAutoTypeClass<GObjectClass> iclass(iface_gtype);

            if (g_object_class_find_property(iclass, canonical_name))
                return lazy_define_gobject_property(cx, obj, id, resolved, name);
        }
    }

    for (i = 0; i < n_interfaces; i++) {
        GIInterfaceInfo* iface_info = interfaces[i];
        GjsAutoFunctionInfo method_info =
            g_interface_info_find_method(iface_info, name);
        if (method_info) {
            if (g_function_info_get_flags (method_info) & GI_FUNCTION_IS_METHOD) {
                bool found = false;
                if (!resolve_on_interface_prototype(cx, iface_info, id, obj,
                                                    &found))
                    return false;

                // Fallback to defining the function from type info...
                if (!found &&
                    !gjs_define_function(cx, obj, m_gtype, method_info))
                    return false;

                *resolved = true;
                return true;
            }
        }


        /* If the name refers to a GObject property, lazily define the property
         * in JS as we do below in the real resolve hook. We ignore fields here
         * because I don't think interfaces can have fields */
        if (canonical_name &&
            is_ginterface_property_name(iface_info, canonical_name)) {
            GjsAutoTypeClass<GObjectClass> oclass(m_gtype);
            // unowned
            GParamSpec* pspec = g_object_class_find_property(
                oclass, canonical_name);  // unowned
            if (pspec && pspec->owner_type == m_gtype) {
                return lazy_define_gobject_property(cx, obj, id, resolved,
                                                    name);
            }
        }

        return resolve_on_interface_prototype(cx, iface_info, id, obj,
                                              resolved);
    }

    *resolved = false;
    return true;
}

[[nodiscard]] static bool is_gobject_property_name(GIObjectInfo* info,
                                                   const char* name) {
    // Optimization: GObject property names must start with a letter
    if (!g_ascii_isalpha(name[0]))
        return false;

    int n_props = g_object_info_get_n_properties(info);
    int n_ifaces = g_object_info_get_n_interfaces(info);
    int ix;

    GjsAutoChar canonical_name = gjs_hyphen_from_camel(name);
    canonicalize_key(canonical_name);

    for (ix = 0; ix < n_props; ix++) {
        GjsAutoPropertyInfo prop_info = g_object_info_get_property(info, ix);
        if (strcmp(canonical_name, prop_info.name()) == 0)
            return true;
    }

    for (ix = 0; ix < n_ifaces; ix++) {
        GjsAutoInterfaceInfo iface_info = g_object_info_get_interface(info, ix);
        if (is_ginterface_property_name(iface_info, canonical_name))
            return true;
    }
    return false;
}

// 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();
}

bool ObjectPrototype::resolve_impl(JSContext* context, JS::HandleObject obj,
                                   JS::HandleId id, bool* resolved) {
    if (m_unresolvable_cache.has(id)) {
        *resolved = false;
        return true;
    }

    JS::UniqueChars prop_name;
    if (!gjs_get_string_id(context, id, &prop_name))
        return false;
    if (!prop_name) {
        *resolved = false;
        return true;  // not resolved, but no error
    }

    if (!uncached_resolve(context, obj, id, prop_name.get(), resolved))
        return false;

    if (!*resolved && !m_unresolvable_cache.putNew(id)) {
        JS_ReportOutOfMemory(context);
        return false;
    }

    return true;
}

bool ObjectPrototype::uncached_resolve(JSContext* context, JS::HandleObject obj,
                                       JS::HandleId id, const char* name,
                                       bool* resolved) {
    // If we have no GIRepository information (we're a JS GObject subclass or an
    // internal non-introspected class such as GLocalFile), we need to look at
    // exposing interfaces. Look up our interfaces through GType data, and then
    // hope that *those* are introspectable.
    if (!info())
        return resolve_no_info(context, obj, id, resolved, name,
                               ConsiderMethodsAndProperties);

    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
         * vfunc on the base class, without any other "real" implementations
         * in the way. If we want to expose a "real" vfunc implementation,
         * we need to go down to the parent infos and look at their VFuncInfos.
         *
         * This is good, but it's memory-hungry -- we would define every
         * possible vfunc on every possible object, even if it's the same
         * "real" vfunc underneath. Instead, only expose vfuncs that are
         * different from their parent, and let prototype chaining do the
         * rest.
         */

        const char *name_without_vfunc_ = &(name[6]);  /* lifetime tied to name */
        bool defined_by_parent;
        GjsAutoVFuncInfo vfunc = find_vfunc_on_parents(
            m_info, name_without_vfunc_, &defined_by_parent);
        if (vfunc) {
            /* In the event that the vfunc is unchanged, let regular
             * prototypal inheritance take over. */
            if (defined_by_parent && is_vfunc_unchanged(vfunc)) {
                *resolved = false;
                return true;
            }

            if (!gjs_define_function(context, obj, m_gtype, vfunc))
                return false;

            *resolved = true;
            return true;
        }

        /* If the vfunc wasn't found, fall through, back to normal
         * method resolution. */
    }

    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);
    if (field_info) {
        bool found = false;
        if (!JS_AlreadyHasOwnPropertyById(context, obj, id, &found))
            return false;
        if (found) {
            *resolved = false;
            return true;
        }

        debug_jsprop("Defining lazy GObject field", id, obj);

        unsigned flags = GJS_MODULE_PROP_FLAGS;
        if (!(g_field_info_get_flags(field_info) & GI_FIELD_IS_WRITABLE))
            flags |= JSPROP_READONLY;

        JS::RootedString key(context, id.toString());
        if (!m_field_cache.putNew(key, field_info.release())) {
            JS_ReportOutOfMemory(context);
            return false;
        }

        JS::RootedValue private_id(context, JS::StringValue(key));
        if (!gjs_define_property_dynamic(
                context, obj, name, "gobject_field", &ObjectBase::field_getter,
                &ObjectBase::field_setter, private_id, flags))
            return false;

        *resolved = true;
        return true;
    }

    /* find_method does not look at methods on parent classes,
     * we rely on javascript to walk up the __proto__ chain
     * and find those and define them in the right prototype.
     *
     * Note that if it isn't a method on the object, since JS
     * lacks multiple inheritance, we're sticking the iface
     * methods in the object prototype, which means there are many
     * copies of the iface methods (one per object class node that
     * introduces the iface)
     */

    GjsAutoBaseInfo implementor_info;
    GjsAutoFunctionInfo method_info =
        g_object_info_find_method_using_interfaces(m_info, name,
                                                   implementor_info.out());

    /**
     * Search through any interfaces implemented by the GType;
     * See https://bugzilla.gnome.org/show_bug.cgi?id=632922
     * for background on why we need to do this.
     */
    if (!method_info)
        return resolve_no_info(context, obj, id, resolved, name,
                               ConsiderOnlyMethods);

#if GJS_VERBOSE_ENABLE_GI_USAGE
    _gjs_log_info_usage(method_info);
#endif

    if (g_function_info_get_flags (method_info) & GI_FUNCTION_IS_METHOD) {
        gjs_debug(GJS_DEBUG_GOBJECT,
                  "Defining method %s in prototype for %s (%s.%s)",
                  method_info.name(), type_name(), ns(), this->name());
        if (GI_IS_INTERFACE_INFO(implementor_info)) {
            bool found = false;
            if (!resolve_on_interface_prototype(context, implementor_info, id,
                                                obj, &found))
                return false;

            // If the method was not found fallback to defining the function
            // from type info...
            if (!found &&
                !gjs_define_function(context, obj, m_gtype, method_info)) {
                return false;
            }
        } else if (!gjs_define_function(context, obj, m_gtype, method_info)) {
            return false;
        }

        *resolved = true; /* we defined the prop in obj */
    }

    return true;
}

bool ObjectPrototype::new_enumerate_impl(JSContext* cx, JS::HandleObject,
                                         JS::MutableHandleIdVector properties,
                                         bool only_enumerable
                                         [[maybe_unused]]) {
    unsigned n_interfaces;
    GType* interfaces = g_type_interfaces(gtype(), &n_interfaces);

    for (unsigned k = 0; k < n_interfaces; k++) {
        GjsAutoInterfaceInfo iface_info =
            g_irepository_find_by_gtype(nullptr, interfaces[k]);

        if (!iface_info) {
            continue;
        }

        int n_methods = g_interface_info_get_n_methods(iface_info);
        int n_properties = g_interface_info_get_n_properties(iface_info);
        if (!properties.reserve(properties.length() + n_methods +
                                n_properties)) {
            JS_ReportOutOfMemory(cx);
            return false;
        }

        // Methods
        for (int i = 0; i < n_methods; i++) {
            GjsAutoFunctionInfo meth_info =
                g_interface_info_get_method(iface_info, i);
            GIFunctionInfoFlags flags = g_function_info_get_flags(meth_info);

            if (flags & GI_FUNCTION_IS_METHOD) {
                const char* name = meth_info.name();
                jsid id = gjs_intern_string_to_id(cx, name);
                if (id.isVoid())
                    return false;
                properties.infallibleAppend(id);
            }
        }

        // Properties
        for (int i = 0; i < n_properties; i++) {
            GjsAutoPropertyInfo prop_info =
                g_interface_info_get_property(iface_info, i);

            GjsAutoChar js_name = gjs_hyphen_to_underscore(prop_info.name());

            jsid id = gjs_intern_string_to_id(cx, js_name);
            if (id.isVoid())
                return false;
            properties.infallibleAppend(id);
        }
    }

    g_free(interfaces);

    if (info()) {
        int n_methods = g_object_info_get_n_methods(info());
        int n_properties = g_object_info_get_n_properties(info());
        if (!properties.reserve(properties.length() + n_methods +
                                n_properties)) {
            JS_ReportOutOfMemory(cx);
            return false;
        }

        // Methods
        for (int i = 0; i < n_methods; i++) {
            GjsAutoFunctionInfo meth_info = g_object_info_get_method(info(), i);
            GIFunctionInfoFlags flags = g_function_info_get_flags(meth_info);

            if (flags & GI_FUNCTION_IS_METHOD) {
                const char* name = meth_info.name();
                jsid id = gjs_intern_string_to_id(cx, name);
                if (id.isVoid())
                    return false;
                properties.infallibleAppend(id);
            }
        }

        // Properties
        for (int i = 0; i < n_properties; i++) {
            GjsAutoPropertyInfo prop_info =
                g_object_info_get_property(info(), i);

            GjsAutoChar js_name = gjs_hyphen_to_underscore(prop_info.name());
            jsid id = gjs_intern_string_to_id(cx, js_name);
            if (id.isVoid())
                return false;
            properties.infallibleAppend(id);
        }
    }

    return true;
}

/* Set properties from args to constructor (args[0] is supposed to be
 * a hash) */
bool ObjectPrototype::props_to_g_parameters(JSContext* context,
                                            JS::HandleObject props,
                                            std::vector<const char*>* names,
                                            AutoGValueVector* values) {
    size_t ix, length;
    JS::RootedId prop_id(context);
    JS::RootedValue value(context);
    JS::Rooted<JS::IdVector> ids(context, context);
    std::unordered_set<GParamSpec*> visited_params;
    if (!JS_Enumerate(context, props, &ids)) {
        gjs_throw(context, "Failed to create property iterator for object props hash");
        return false;
    }

    values->reserve(ids.length());
    for (ix = 0, length = ids.length(); ix < length; ix++) {
        /* ids[ix] is reachable because props is rooted, but require_property
         * doesn't know that */
        prop_id = ids[ix];

        if (!prop_id.isString())
            return gjs_wrapper_throw_nonexistent_field(
                context, m_gtype, gjs_debug_id(prop_id).c_str());

        JS::RootedString js_prop_name(context, prop_id.toString());
        GParamSpec *param_spec = find_param_spec_from_id(context, js_prop_name);
        if (!param_spec)
            return false;

        if (visited_params.find(param_spec) != visited_params.end())
            continue;
        visited_params.insert(param_spec);

        if (!JS_GetPropertyById(context, props, prop_id, &value))
            return false;
        if (value.isUndefined()) {
            gjs_throw(context, "Invalid value 'undefined' for property %s in "
                      "object initializer.", param_spec->name);
            return false;
        }

        if (!(param_spec->flags & G_PARAM_WRITABLE))
            return gjs_wrapper_throw_readonly_field(context, m_gtype,
                                                    param_spec->name);
            /* prevent setting the prop even in JS */

        Gjs::AutoGValue& gvalue =
            values->emplace_back(G_PARAM_SPEC_VALUE_TYPE(param_spec));
        if (!gjs_value_to_g_value(context, value, &gvalue))
            return false;

        names->push_back(param_spec->name);  /* owned by GParamSpec in cache */
    }

    return true;
}

void ObjectInstance::wrapped_gobj_dispose_notify(
    void* data, GObject* where_the_object_was GJS_USED_VERBOSE_LIFECYCLE) {
    auto *priv = static_cast<ObjectInstance *>(data);
    priv->gobj_dispose_notify();
    gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Wrapped GObject %p disposed",
                        where_the_object_was);
}

void ObjectInstance::track_gobject_finalization() {
    auto quark = ObjectBase::disposed_quark();
    g_object_steal_qdata(m_ptr, quark);
    g_object_set_qdata_full(m_ptr, quark, this, [](void* data) {
        auto* self = static_cast<ObjectInstance*>(data);
        self->m_gobj_finalized = true;
        gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Wrapped GObject %p finalized",
                            self->m_ptr.get());
    });
}

void ObjectInstance::ignore_gobject_finalization() {
    auto quark = ObjectBase::disposed_quark();
    if (g_object_get_qdata(m_ptr, quark) == this) {
        g_object_steal_qdata(m_ptr, quark);
        g_object_set_qdata(m_ptr, quark, gjs_int_to_pointer(DISPOSED_OBJECT));
    }
}

void
ObjectInstance::gobj_dispose_notify(void)
{
    m_gobj_disposed = true;

    unset_object_qdata();
    track_gobject_finalization();

    if (m_uses_toggle_ref) {
        g_object_ref(m_ptr.get());
        g_object_remove_toggle_ref(m_ptr, wrapped_gobj_toggle_notify, this);
        ToggleQueue::get_default()->cancel(this);
        wrapped_gobj_toggle_notify(this, m_ptr, TRUE);
        m_uses_toggle_ref = false;
    }

    if (GjsContextPrivate::from_current_context()->is_owner_thread())
        discard_wrapper();
}

void ObjectInstance::remove_wrapped_gobjects_if(
    const ObjectInstance::Predicate& predicate,
    const ObjectInstance::Action& action) {
    // Note: remove_if() does not actually remove elements, just reorders them
    // and returns a start iterator of elements to remove
    s_wrapped_gobject_list.erase(
        std::remove_if(s_wrapped_gobject_list.begin(),
                       s_wrapped_gobject_list.end(),
                       ([predicate, action](ObjectInstance* link) {
                           if (predicate(link)) {
                               action(link);
                               return true;
                           }
                           return false;
                       })),
        s_wrapped_gobject_list.end());
}

/*
 * ObjectInstance::context_dispose_notify:
 *
 * Callback called when the #GjsContext is disposed. It just calls
 * handle_context_dispose() on every ObjectInstance.
 */
void ObjectInstance::context_dispose_notify(void*, GObject* where_the_object_was
                                            [[maybe_unused]]) {
    std::for_each(s_wrapped_gobject_list.begin(), s_wrapped_gobject_list.end(),
        std::mem_fn(&ObjectInstance::handle_context_dispose));
}

/*
 * ObjectInstance::handle_context_dispose:
 *
 * Called on each existing ObjectInstance when the #GjsContext is disposed.
 */
void ObjectInstance::handle_context_dispose(void) {
    if (wrapper_is_rooted()) {
        debug_lifecycle("Was rooted, but unrooting due to GjsContext dispose");
        discard_wrapper();
    }
}

void
ObjectInstance::toggle_down(void)
{
    debug_lifecycle("Toggle notify DOWN");

    /* Change to weak ref so the wrapper-wrappee pair can be
     * collected by the GC
     */
    if (wrapper_is_rooted()) {
        debug_lifecycle("Unrooting wrapper");
        GjsContextPrivate* gjs = GjsContextPrivate::from_current_context();
        switch_to_unrooted(gjs->context());

        /* During a GC, the collector asks each object which other
         * objects that it wants to hold on to so if there's an entire
         * section of the heap graph that's not connected to anything
         * else, and not reachable from the root set, then it can be
         * trashed all at once.
         *
         * GObjects, however, don't work like that, there's only a
         * reference count but no notion of who owns the reference so,
         * a JS object that's wrapping a GObject is unconditionally held
         * alive as long as the GObject has >1 references.
         *
         * Since we cannot know how many more wrapped GObjects are going
         * be marked for garbage collection after the owner is destroyed,
         * always queue a garbage collection when a toggle reference goes
         * down.
         */
        if (!gjs->destroying())
            gjs->schedule_gc();
    }
}

void
ObjectInstance::toggle_up(void)
{
    if (G_UNLIKELY(!m_ptr || m_gobj_disposed || m_gobj_finalized)) {
        if (m_ptr) {
            gjs_debug_lifecycle(
                GJS_DEBUG_GOBJECT,
                "Avoid to toggle up a wrapper for a %s object: %p (%s)",
                m_gobj_finalized ? "finalized" : "disposed", m_ptr.get(),
                g_type_name(gtype()));
        } else {
            gjs_debug_lifecycle(
                GJS_DEBUG_GOBJECT,
                "Avoid to toggle up a wrapper for a released %s object (%p)",
                g_type_name(gtype()), this);
        }
        return;
    }

    /* We need to root the JSObject associated with the passed in GObject so it
     * doesn't get garbage collected (and lose any associated javascript state
     * such as custom properties).
     */
    if (!has_wrapper()) /* Object already GC'd */
        return;

    debug_lifecycle("Toggle notify UP");

    /* Change to strong ref so the wrappee keeps the wrapper alive
     * in case the wrapper has data in it that the app cares about
     */
    if (!wrapper_is_rooted()) {
        // FIXME: thread the context through somehow. Maybe by looking up the
        // realm that obj belongs to.
        debug_lifecycle("Rooting wrapper");
        auto* cx = GjsContextPrivate::from_current_context()->context();
        switch_to_rooted(cx);
    }
}

static void toggle_handler(ObjectInstance* self,
                           ToggleQueue::Direction direction) {
    switch (direction) {
        case ToggleQueue::UP:
            self->toggle_up();
            break;
        case ToggleQueue::DOWN:
            self->toggle_down();
            break;
        default:
            g_assert_not_reached();
    }
}

void ObjectInstance::wrapped_gobj_toggle_notify(void* instance, GObject*,
                                                gboolean is_last_ref) {
    bool is_main_thread;
    bool toggle_up_queued, toggle_down_queued;
    auto* self = static_cast<ObjectInstance*>(instance);

    GjsContextPrivate* gjs = GjsContextPrivate::from_current_context();
    if (gjs->destroying()) {
        /* Do nothing here - we're in the process of disassociating
         * the objects.
         */
        return;
    }

    /* We only want to touch javascript from one thread.
     * If we're not in that thread, then we need to defer processing
     * to it.
     * In case we're toggling up (and thus rooting the JS object) we
     * also need to take care if GC is running. The marking side
     * of it is taken care by JS::Heap, which we use in GjsMaybeOwned,
     * so we're safe. As for sweeping, it is too late: the JS object
     * is dead, and attempting to keep it alive would soon crash
     * the process. Plus, if we touch the JSAPI from another thread, libmozjs
     * aborts in most cases when in debug mode.
     * Thus, we drain the toggle queue when GC starts, in order to
     * prevent this from happening.
     * In practice, a toggle up during JS finalize can only happen
     * for temporary refs/unrefs of objects that are garbage anyway,
     * because JS code is never invoked while the finalizers run
     * and C code needs to clean after itself before it returns
     * from dispose()/finalize().
     * On the other hand, toggling down is a lot simpler, because
     * we're creating more garbage. So we just unroot the object, make it a
     * weak pointer, and wait for the next GC cycle.
     *
     * Note that one would think that toggling up only happens
     * in the main thread (because toggling up is the result of
     * the JS object, previously visible only to JS code, becoming
     * visible to the refcounted C world), but because of weird
     * weak singletons like g_bus_get_sync() objects can see toggle-ups
     * from different threads too.
     */
    is_main_thread = gjs->is_owner_thread();

    auto toggle_queue = ToggleQueue::get_default();
    std::tie(toggle_down_queued, toggle_up_queued) =
        toggle_queue->is_queued(self);
    bool anything_queued = toggle_up_queued || toggle_down_queued;

    if (is_last_ref) {
        /* We've transitions from 2 -> 1 references,
         * The JSObject is rooted and we need to unroot it so it
         * can be garbage collected
         */
        if (is_main_thread && !anything_queued) {
            self->toggle_down();
        } else {
            toggle_queue->enqueue(self, ToggleQueue::DOWN, toggle_handler);
        }
    } else {
        /* We've transitioned from 1 -> 2 references.
         *
         * The JSObject associated with the gobject is not rooted,
         * but it needs to be. We'll root it.
         */
        if (is_main_thread && !anything_queued &&
            !JS::RuntimeHeapIsCollecting()) {
            self->toggle_up();
        } else {
            toggle_queue->enqueue(self, ToggleQueue::UP, toggle_handler);
        }
    }
}

void
ObjectInstance::release_native_object(void)
{
    discard_wrapper();

    if (m_gobj_finalized) {
        g_critical(
            "Object %p of type %s has been finalized while it was still "
            "owned by gjs, this is due to invalid memory management.",
            m_ptr.get(), g_type_name(gtype()));
        m_ptr.release();
        return;
    }

    if (m_ptr)
        gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Releasing native object %s %p",
                            g_type_name(gtype()), m_ptr.get());

    if (m_gobj_disposed)
        ignore_gobject_finalization();

    if (m_uses_toggle_ref && !m_gobj_disposed)
        g_object_remove_toggle_ref(m_ptr.release(), wrapped_gobj_toggle_notify,
                                   this);
    else
        m_ptr = nullptr;
}

/* At shutdown, we need to ensure we've cleared the context of any
 * pending toggle references.
 */
void
gjs_object_clear_toggles(void)
{
    ToggleQueue::get_default()->handle_all_toggles(toggle_handler);
}

void
gjs_object_shutdown_toggle_queue(void)
{
    ToggleQueue::get_default()->shutdown();
}

/*
 * ObjectInstance::prepare_shutdown:
 *
 * Called when the #GjsContext is disposed, in order to release all GC roots of
 * JSObjects that are held by GObjects.
 */
void ObjectInstance::prepare_shutdown(void) {
    /* We iterate over all of the objects, breaking the JS <-> C
     * association.  We avoid the potential recursion implied in:
     *   toggle ref removal -> gobj dispose -> toggle ref notify
     * by emptying the toggle queue earlier in the shutdown sequence. */
    ObjectInstance::remove_wrapped_gobjects_if(
        std::mem_fn(&ObjectInstance::wrapper_is_rooted),
        std::mem_fn(&ObjectInstance::release_native_object));
}

ObjectInstance::ObjectInstance(ObjectPrototype* prototype,
                               JS::HandleObject object)
    : GIWrapperInstance(prototype, object),
      m_wrapper_finalized(false),
      m_gobj_disposed(false),
      m_gobj_finalized(false),
      m_uses_toggle_ref(false) {
    GTypeQuery query;
    type_query_dynamic_safe(&query);
    if (G_LIKELY(query.type))
        JS::AddAssociatedMemory(object, query.instance_size,
                                MemoryUse::GObjectInstanceStruct);

    GJS_INC_COUNTER(object_instance);
}

ObjectPrototype::ObjectPrototype(GIObjectInfo* info, GType gtype)
    : GIWrapperPrototype(info, gtype) {
    g_type_class_ref(gtype);

    GJS_INC_COUNTER(object_prototype);
}

/*
 * ObjectInstance::update_heap_wrapper_weak_pointers:
 *
 * Private callback, called after the JS engine finishes garbage collection, and
 * notifies when weak pointers need to be either moved or swept.
 */
void ObjectInstance::update_heap_wrapper_weak_pointers(JSTracer* trc,
                                                       JS::Compartment*,
                                                       void*) {
    gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Weak pointer update callback, "
                        "%zu wrapped GObject(s) to examine",
                        ObjectInstance::num_wrapped_gobjects());

    // Take a lock on the queue till we're done with it, so that we don't
    // risk that another thread will queue something else while sweeping
    auto locked_queue = ToggleQueue::get_default();

    ObjectInstance::remove_wrapped_gobjects_if(
        [&trc](ObjectInstance* instance) -> bool {
            return instance->weak_pointer_was_finalized(trc);
        },
        std::mem_fn(&ObjectInstance::disassociate_js_gobject));

    s_wrapped_gobject_list.shrink_to_fit();
}

bool ObjectInstance::weak_pointer_was_finalized(JSTracer* trc) {
    if (has_wrapper() && !wrapper_is_rooted()) {
        bool toggle_down_queued, toggle_up_queued;

        auto toggle_queue = ToggleQueue::get_default();
        std::tie(toggle_down_queued, toggle_up_queued) =
            toggle_queue->is_queued(this);

        if (!toggle_down_queued && toggle_up_queued)
            return false;

        if (!update_after_gc(trc))
            return false;

        if (toggle_down_queued)
            toggle_queue->cancel(this);

        /* Ouch, the JS object is dead already. Disassociate the
         * GObject and hope the GObject dies too. (Remove it from
         * the weak pointer list first, since the disassociation
         * may also cause it to be erased.)
         */
        debug_lifecycle("Found GObject weak pointer whose JS wrapper is about "
                        "to be finalized");
        return true;
    }
    return false;
}

/*
 * ObjectInstance::ensure_weak_pointer_callback:
 *
 * Private method called when adding a weak pointer for the first time.
 */
void ObjectInstance::ensure_weak_pointer_callback(JSContext* cx) {
    if (!s_weak_pointer_callback) {
        JS_AddWeakPointerCompartmentCallback(
            cx, &ObjectInstance::update_heap_wrapper_weak_pointers, nullptr);
        s_weak_pointer_callback = true;
    }
}

void
ObjectInstance::associate_js_gobject(JSContext       *context,
                                     JS::HandleObject object,
                                     GObject         *gobj)
{
    g_assert(!wrapper_is_rooted());

    m_uses_toggle_ref = false;
    m_ptr = gobj;
    set_object_qdata();
    m_wrapper = object;
    m_gobj_disposed = !!g_object_get_qdata(gobj, ObjectBase::disposed_quark());

    ensure_weak_pointer_callback(context);
    link();

    if (!G_UNLIKELY(m_gobj_disposed))
        g_object_weak_ref(gobj, wrapped_gobj_dispose_notify, this);
}

bool ObjectInstance::ensure_uses_toggle_ref(JSContext* cx) {
    if (m_uses_toggle_ref)
        return true;

    if (!check_gobject_disposed_or_finalized("add toggle reference on"))
        return true;

    debug_lifecycle("Switching object instance to toggle ref");

    g_assert(!wrapper_is_rooted());

    /* OK, here is where things get complicated. We want the
     * wrapped gobj to keep the JSObject* wrapper alive, because
     * people might set properties on the JSObject* that they care
     * about. Therefore, whenever the refcount on the wrapped gobj
     * is >1, i.e. whenever something other than the wrapper is
     * referencing the wrapped gobj, the wrapped gobj has a strong
     * ref (gc-roots the wrapper). When the refcount on the
     * wrapped gobj is 1, then we change to a weak ref to allow
     * the wrapper to be garbage collected (and thus unref the
     * wrappee).
     */
    m_uses_toggle_ref = true;
    switch_to_rooted(cx);
    g_object_add_toggle_ref(m_ptr, wrapped_gobj_toggle_notify, this);

    /* 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_ptr);

    return true;
}

static void invalidate_closure_vector(std::vector<GClosure*>* closures,
                                      void* data, GClosureNotify notify_func) {
    g_assert(closures);
    g_assert(notify_func);

    for (auto it = closures->begin(); it != closures->end();) {
        // This will also free the closure data, through the closure
        // invalidation mechanism, but adding a temporary reference to
        // ensure that the closure is still valid when calling invalidation
        // notify callbacks
        GjsAutoGClosure closure(*it, GjsAutoTakeOwnership());
        it = closures->erase(it);

        // Only call the invalidate notifiers that won't touch this vector
        g_closure_remove_invalidate_notifier(closure, data, notify_func);
        g_closure_invalidate(closure);
    }

    g_assert(closures->empty());
}

// Note: m_wrapper (the JS object) may already be null when this is called, if
// it was finalized while the GObject was toggled down.
void
ObjectInstance::disassociate_js_gobject(void)
{
    bool had_toggle_down, had_toggle_up;

    auto locked_queue = ToggleQueue::get_default();
    std::tie(had_toggle_down, had_toggle_up) = locked_queue->cancel(this);
    if (had_toggle_up && !had_toggle_down) {
        g_error(
            "JS object wrapper for GObject %p (%s) is being released while "
            "toggle references are still pending.",
            m_ptr.get(), type_name());
    }

    if (!m_gobj_disposed)
        g_object_weak_unref(m_ptr.get(), wrapped_gobj_dispose_notify, this);

    if (!m_gobj_finalized) {
        /* Fist, remove the wrapper pointer from the wrapped GObject */
        unset_object_qdata();
    }

    /* Now release all the resources the current wrapper has */
    invalidate_closures();
    release_native_object();

    /* Mark that a JS object once existed, but it doesn't any more */
    m_wrapper_finalized = true;
}

bool ObjectInstance::init_impl(JSContext* context, const JS::CallArgs& args,
                               JS::HandleObject object) {
    g_assert(gtype() != G_TYPE_NONE);

    if (args.length() > 1 &&
        !JS::WarnUTF8(context,
                      "Too many arguments to the constructor of %s: expected "
                      "1, got %u",
                      name(), args.length()))
        return false;

    std::vector<const char *> names;
    AutoGValueVector values;

    if (args.length() > 0 && !args[0].isUndefined()) {
        if (!args[0].isObject()) {
            gjs_throw(context,
                      "Argument to the constructor of %s should be an object "
                      "with properties to set",
                      name());
            return false;
        }

        JS::RootedObject props(context, &args[0].toObject());
        if (!m_proto->props_to_g_parameters(context, props, &names, &values))
            return false;
    }

    if (G_TYPE_IS_ABSTRACT(gtype())) {
        gjs_throw(context,
                  "Cannot instantiate abstract type %s", g_type_name(gtype()));
        return false;
    }

    // Mark this object in the construction stack, it will be popped in
    // gjs_object_custom_init() in gi/gobject.cpp.
    if (is_custom_js_class()) {
        GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context);
        if (!gjs->object_init_list().append(object)) {
            JS_ReportOutOfMemory(context);
            return false;
        }
    }

    g_assert(names.size() == values.size());
    GObject* gobj = g_object_new_with_properties(gtype(), values.size(),
                                                 names.data(), values.data());

    ObjectInstance *other_priv = ObjectInstance::for_gobject(gobj);
    if (other_priv && other_priv->m_wrapper != object.get()) {
        /* g_object_new_with_properties() returned an object that's already
         * tracked by a JS object.
         *
         * This typically occurs in one of two cases:
         * - This object is a singleton like IBus.IBus
         * - This object passed itself to JS before g_object_new_* returned
         *
         * In these cases, return the existing JS wrapper object instead
         * of creating a new one.
         *
         * 'object' has a value that was originally created by
         * JS_NewObjectForConstructor in GJS_NATIVE_CONSTRUCTOR_PRELUDE, but
         * we're not actually using it, so just let it get collected. Avoiding
         * this would require a non-trivial amount of work.
         * */
        bool toggle_ref_added = false;
        if (!m_uses_toggle_ref) {
            if (!other_priv->ensure_uses_toggle_ref(context)) {
                gjs_throw(context,
                          "Impossible to set toggle references on %sobject %p",
                          other_priv->m_gobj_disposed ? "disposed " : "", gobj);
                return false;
            }

            toggle_ref_added = m_uses_toggle_ref;
        }

        args.rval().setObject(*other_priv->m_wrapper);

        if (toggle_ref_added)
            g_clear_object(&gobj); /* We already own a reference */
        return true;
    }

    if (G_IS_INITIALLY_UNOWNED(gobj) &&
        !g_object_is_floating(gobj)) {
        /* GtkWindow does not return a ref to caller of g_object_new.
         * Need a flag in gobject-introspection to tell us this.
         */
        gjs_debug(GJS_DEBUG_GOBJECT,
                  "Newly-created object is initially unowned but we did not get the "
                  "floating ref, probably GtkWindow, using hacky workaround");
        g_object_ref(gobj);
    } else if (g_object_is_floating(gobj)) {
        g_object_ref_sink(gobj);
    } else {
        /* we should already have a ref */
    }

    if (!m_ptr)
        associate_js_gobject(context, object, gobj);

    TRACE(GJS_OBJECT_WRAPPER_NEW(this, m_ptr, ns(), name()));

    args.rval().setObject(*object);
    return true;
}

// See GIWrapperBase::constructor()
bool ObjectInstance::constructor_impl(JSContext* context,
                                      JS::HandleObject object,
                                      const JS::CallArgs& argv) {
    JS::RootedValue initer(context);
    GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context);
    const auto& new_target = argv.newTarget();
    bool has_gtype;

    g_assert(new_target.isObject() && "new.target needs to be an object");
    JS::RootedObject rooted_target(context, &new_target.toObject());
    if (!JS_HasOwnPropertyById(context, rooted_target, gjs->atoms().gtype(),
                               &has_gtype))
        return false;

    if (!has_gtype) {
        gjs_throw(context,
                  "Tried to construct an object without a GType; are "
                  "you using GObject.registerClass() when inheriting "
                  "from a GObject type?");
        return false;
    }

    return gjs_object_require_property(context, object, "GObject instance",
                                       gjs->atoms().init(), &initer) &&
           gjs->call_function(object, initer, argv, argv.rval());
}

void ObjectInstance::trace_impl(JSTracer* tracer) {
    for (GClosure *closure : m_closures)
        Gjs::Closure::for_gclosure(closure)->trace(tracer);
}

void ObjectPrototype::trace_impl(JSTracer* tracer) {
    m_property_cache.trace(tracer);
    m_field_cache.trace(tracer);
    m_unresolvable_cache.trace(tracer);
    for (GClosure* closure : m_vfuncs)
        Gjs::Closure::for_gclosure(closure)->trace(tracer);
}

void ObjectInstance::finalize_impl(JS::GCContext* gcx, JSObject* obj) {
    GTypeQuery query;
    type_query_dynamic_safe(&query);
    if (G_LIKELY(query.type))
        JS::RemoveAssociatedMemory(obj, query.instance_size,
                                   MemoryUse::GObjectInstanceStruct);

    GIWrapperInstance::finalize_impl(gcx, obj);
}

ObjectInstance::~ObjectInstance() {
    TRACE(GJS_OBJECT_WRAPPER_FINALIZE(this, m_ptr, ns(), name()));

    invalidate_closures();

    // Do not keep the queue locked here, as we may want to leave the other
    // threads to queue toggle events till we're owning the GObject so that
    // eventually (once the toggle reference is finally removed) we can be
    // sure that no other toggle event will target this (soon dead) wrapper.
    bool had_toggle_up;
    bool had_toggle_down;
    std::tie(had_toggle_down, had_toggle_up) =
        ToggleQueue::get_default()->cancel(this);

    /* GObject is not already freed */
    if (m_ptr) {
        if (!had_toggle_up && had_toggle_down) {
            g_error(
                "Finalizing wrapper for an object that's scheduled to be "
                "unrooted: %s.%s\n",
                ns(), name());
        }

        if (!m_gobj_disposed)
            g_object_weak_unref(m_ptr, wrapped_gobj_dispose_notify, this);

        if (!m_gobj_finalized)
            unset_object_qdata();

        bool was_using_toggle_refs = m_uses_toggle_ref;
        release_native_object();

        if (was_using_toggle_refs) {
            // We need to cancel again, to be sure that no other thread added
            // another toggle reference before we were removing the last one.
            ToggleQueue::get_default()->cancel(this);
        }
    }

    if (wrapper_is_rooted()) {
        /* This happens when the refcount on the object is still >1,
         * for example with global objects GDK never frees like GdkDisplay,
         * when we close down the JS runtime.
         */
        gjs_debug(GJS_DEBUG_GOBJECT,
                  "Wrapper was finalized despite being kept alive, has refcount >1");

        debug_lifecycle("Unrooting object");

        discard_wrapper();
    }
    unlink();

    GJS_DEC_COUNTER(object_instance);
}

ObjectPrototype::~ObjectPrototype() {
    invalidate_closure_vector(&m_vfuncs, this, &vfunc_invalidated_notify);

    g_type_class_unref(g_type_class_peek(m_gtype));

    GJS_DEC_COUNTER(object_prototype);
}

JSObject* gjs_lookup_object_constructor_from_info(JSContext* context,
                                                  GIObjectInfo* info,
                                                  GType gtype) {
    JS::RootedObject in_object(context);
    const char *constructor_name;

    if (info) {
        in_object = gjs_lookup_namespace_object(context, (GIBaseInfo*) info);
        constructor_name = g_base_info_get_name((GIBaseInfo*) info);
    } else {
        in_object = gjs_lookup_private_namespace(context);
        constructor_name = g_type_name(gtype);
    }

    if (G_UNLIKELY (!in_object))
        return NULL;

    bool found;
    if (!JS_HasProperty(context, in_object, constructor_name, &found))
        return NULL;

    JS::RootedValue value(context);
    if (found && !JS_GetProperty(context, in_object, constructor_name, &value))
        return NULL;

    JS::RootedObject constructor(context);
    if (value.isUndefined()) {
        /* In case we're looking for a private type, and we don't find it,
           we need to define it first.
        */
        JS::RootedObject ignored(context);
        if (!ObjectPrototype::define_class(context, in_object, nullptr, gtype,
                                           nullptr, 0, &constructor, &ignored))
            return nullptr;
    } else {
        if (G_UNLIKELY (!value.isObject()))
            return NULL;

        constructor = &value.toObject();
    }

    g_assert(constructor);

    return constructor;
}

GJS_JSAPI_RETURN_CONVENTION
static JSObject *
gjs_lookup_object_prototype_from_info(JSContext    *context,
                                      GIObjectInfo *info,
                                      GType         gtype)
{
    JS::RootedObject constructor(context,
        gjs_lookup_object_constructor_from_info(context, info, gtype));

    if (G_UNLIKELY(!constructor))
        return NULL;

    const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
    JS::RootedObject prototype(context);
    if (!gjs_object_require_property(context, constructor, "constructor object",
                                     atoms.prototype(), &prototype))
        return NULL;

    return prototype;
}

GJS_JSAPI_RETURN_CONVENTION
static JSObject *
gjs_lookup_object_prototype(JSContext *context,
                            GType      gtype)
{
    GjsAutoObjectInfo info = g_irepository_find_by_gtype(nullptr, gtype);
    return gjs_lookup_object_prototype_from_info(context, info, gtype);
}

// Retrieves a GIFieldInfo for a field named @key. This is for use in
// field_getter_impl() and field_setter_not_impl(), where the field info *must*
// have been cached previously in resolve_impl() on this ObjectPrototype or one
// of its parent ObjectPrototypes. This will fail an assertion if there is no
// cached field info.
//
// The caller does not own the return value, and it can never be null.
GIFieldInfo* ObjectPrototype::lookup_cached_field_info(JSContext* cx,
                                                       JS::HandleString key) {
    if (!info()) {
        // Custom JS classes can't have fields, and fields on internal classes
        // are not available. We must be looking up a field on a
        // GObject-introspected parent.
        GType parent_gtype = g_type_parent(m_gtype);
        g_assert(parent_gtype != G_TYPE_INVALID &&
                 "Custom JS class must have parent");
        ObjectPrototype* parent_proto =
            ObjectPrototype::for_gtype(parent_gtype);

        if (!parent_proto) {
            JS::RootedObject proto(cx, gjs_lookup_object_prototype(cx, parent_gtype));
            parent_proto = ObjectPrototype::for_js(cx, proto);
        }

        g_assert(parent_proto &&
                 "Custom JS class's parent must have been accessed in JS");
        return parent_proto->lookup_cached_field_info(cx, key);
    }

    gjs_debug_jsprop(GJS_DEBUG_GOBJECT,
                     "Looking up cached field info for %s in '%s' prototype",
                     gjs_debug_string(key).c_str(), g_type_name(m_gtype));
    auto entry = m_field_cache.lookupForAdd(key);
    if (entry)
        return entry->value().get();

    // We must be looking up a field defined on a parent. Look up the prototype
    // object via its GIObjectInfo.
    GjsAutoObjectInfo parent_info = g_object_info_get_parent(m_info);
    JS::RootedObject parent_proto(cx, gjs_lookup_object_prototype_from_info(
                                          cx, parent_info, G_TYPE_INVALID));
    ObjectPrototype* parent = ObjectPrototype::for_js(cx, parent_proto);
    return parent->lookup_cached_field_info(cx, key);
}

bool ObjectInstance::associate_closure(JSContext* cx, GClosure* closure) {
    if (!is_prototype()) {
        if (!to_instance()->ensure_uses_toggle_ref(cx)) {
            gjs_throw(cx, "Impossible to set toggle references on %sobject %p",
                      m_gobj_disposed ? "disposed " : "", to_instance()->ptr());
            return false;
        }
    }

    g_assert(std::find(m_closures.begin(), m_closures.end(), closure) ==
                 m_closures.end() &&
             "This closure was already associated with this object");

    /* This is a weak reference, and will be cleared when the closure is
     * invalidated */
    m_closures.push_back(closure);
    g_closure_add_invalidate_notifier(
        closure, this, &ObjectInstance::closure_invalidated_notify);

    return true;
}

void ObjectInstance::closure_invalidated_notify(void* data, GClosure* closure) {
    // This callback should *only* touch m_closures
    auto* priv = static_cast<ObjectInstance*>(data);
    Gjs::remove_one_from_unsorted_vector(&priv->m_closures, closure);
}

void ObjectInstance::invalidate_closures() {
    invalidate_closure_vector(&m_closures, this, &closure_invalidated_notify);
    m_closures.shrink_to_fit();
}

bool ObjectBase::connect(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
    if (!priv->check_is_instance(cx, "connect to signals"))
        return false;

    return priv->to_instance()->connect_impl(cx, args, false);
}

bool ObjectBase::connect_after(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
    if (!priv->check_is_instance(cx, "connect to signals"))
        return false;

    return priv->to_instance()->connect_impl(cx, args, true);
}

bool
ObjectInstance::connect_impl(JSContext          *context,
                             const JS::CallArgs& args,
                             bool                after)
{
    gulong id;
    guint signal_id;
    GQuark signal_detail;

    gjs_debug_gsignal("connect obj %p priv %p", m_wrapper.get(), this);

    if (!check_gobject_disposed_or_finalized("connect to any signal on")) {
        args.rval().setInt32(0);
        return true;
    }

    JS::UniqueChars signal_name;
    JS::RootedObject callback(context);
    if (!gjs_parse_call_args(context, after ? "connect_after" : "connect", args, "so",
                             "signal name", &signal_name,
                             "callback", &callback))
        return false;

    std::string dynamicString = format_name() + '.' +
                                (after ? "connect_after" : "connect") + "('" +
                                signal_name.get() + "')";
    AutoProfilerLabel label(context, "", dynamicString.c_str());

    if (!JS::IsCallable(callback)) {
        gjs_throw(context, "second arg must be a callback");
        return false;
    }

    if (!g_signal_parse_name(signal_name.get(), gtype(), &signal_id,
                             &signal_detail, true)) {
        gjs_throw(context, "No signal '%s' on object '%s'",
                  signal_name.get(), type_name());
        return false;
    }

    GClosure* closure = Gjs::Closure::create_for_signal(
        context, callback, "signal callback", signal_id);
    if (closure == NULL)
        return false;
    if (!associate_closure(context, closure))
        return false;

    id = g_signal_connect_closure_by_id(m_ptr, signal_id, signal_detail,
                                        closure, after);

    args.rval().setDouble(id);

    return true;
}

bool ObjectBase::emit(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
    if (!priv->check_is_instance(cx, "emit signal"))
        return false;

    return priv->to_instance()->emit_impl(cx, args);
}

bool
ObjectInstance::emit_impl(JSContext          *context,
                          const JS::CallArgs& argv)
{
    guint signal_id;
    GQuark signal_detail;
    GSignalQuery signal_query;
    unsigned int i;

    gjs_debug_gsignal("emit obj %p priv %p argc %d", m_wrapper.get(), this,
                      argv.length());

    if (!check_gobject_finalized("emit any signal on")) {
        argv.rval().setUndefined();
        return true;
    }

    JS::UniqueChars signal_name;
    if (!gjs_parse_call_args(context, "emit", argv, "!s",
                             "signal name", &signal_name))
        return false;

    std::string dynamicString =
        format_name() + "emit('" + signal_name.get() + "')";
    AutoProfilerLabel label(context, "", dynamicString.c_str());

    if (!g_signal_parse_name(signal_name.get(), gtype(), &signal_id,
                             &signal_detail, false)) {
        gjs_throw(context, "No signal '%s' on object '%s'",
                  signal_name.get(), type_name());
        return false;
    }

    g_signal_query(signal_id, &signal_query);

    if ((argv.length() - 1) != signal_query.n_params) {
        gjs_throw(context, "Signal '%s' on %s requires %d args got %d",
                  signal_name.get(), type_name(), signal_query.n_params,
                  argv.length() - 1);
        return false;
    }

    AutoGValueVector instance_and_args;
    instance_and_args.reserve(signal_query.n_params + 1);
    Gjs::AutoGValue& instance = instance_and_args.emplace_back(gtype());
    g_value_set_instance(&instance, m_ptr);

    for (i = 0; i < signal_query.n_params; ++i) {
        GType gtype = signal_query.param_types[i] & ~G_SIGNAL_TYPE_STATIC_SCOPE;
        Gjs::AutoGValue& value = instance_and_args.emplace_back(gtype);
        if ((signal_query.param_types[i] & G_SIGNAL_TYPE_STATIC_SCOPE) != 0) {
            if (!gjs_value_to_g_value_no_copy(context, argv[i + 1], &value))
                return false;
        } else {
            if (!gjs_value_to_g_value(context, argv[i + 1], &value))
                return false;
        }
    }

    if (signal_query.return_type == G_TYPE_NONE) {
        g_signal_emitv(instance_and_args.data(), signal_id, signal_detail,
                       nullptr);
        argv.rval().setUndefined();
        return true;
    }

    GType gtype = signal_query.return_type & ~G_SIGNAL_TYPE_STATIC_SCOPE;
    Gjs::AutoGValue rvalue(gtype);
    g_signal_emitv(instance_and_args.data(), signal_id, signal_detail, &rvalue);

    return gjs_value_from_g_value(context, argv.rval(), &rvalue);
}

bool ObjectInstance::signal_match_arguments_from_object(
    JSContext* cx, JS::HandleObject match_obj, GSignalMatchType* mask_out,
    unsigned* signal_id_out, GQuark* detail_out,
    JS::MutableHandleObject callable_out) {
    g_assert(mask_out && signal_id_out && detail_out && "forgot out parameter");

    int mask = 0;
    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);

    bool has_id;
    unsigned signal_id = 0;
    if (!JS_HasOwnPropertyById(cx, match_obj, atoms.signal_id(), &has_id))
        return false;
    if (has_id) {
        mask |= G_SIGNAL_MATCH_ID;

        JS::RootedValue value(cx);
        if (!JS_GetPropertyById(cx, match_obj, atoms.signal_id(), &value))
            return false;

        JS::UniqueChars signal_name = gjs_string_to_utf8(cx, value);
        if (!signal_name)
            return false;

        signal_id = g_signal_lookup(signal_name.get(), gtype());
    }

    bool has_detail;
    GQuark detail = 0;
    if (!JS_HasOwnPropertyById(cx, match_obj, atoms.detail(), &has_detail))
        return false;
    if (has_detail) {
        mask |= G_SIGNAL_MATCH_DETAIL;

        JS::RootedValue value(cx);
        if (!JS_GetPropertyById(cx, match_obj, atoms.detail(), &value))
            return false;

        JS::UniqueChars detail_string = gjs_string_to_utf8(cx, value);
        if (!detail_string)
            return false;

        detail = g_quark_from_string(detail_string.get());
    }

    bool has_func;
    JS::RootedObject callable(cx);
    if (!JS_HasOwnPropertyById(cx, match_obj, atoms.func(), &has_func))
        return false;
    if (has_func) {
        mask |= G_SIGNAL_MATCH_CLOSURE;

        JS::RootedValue value(cx);
        if (!JS_GetPropertyById(cx, match_obj, atoms.func(), &value))
            return false;

        if (!value.isObject() || !JS::IsCallable(&value.toObject())) {
            gjs_throw(cx, "'func' property must be a function");
            return false;
        }

        callable = &value.toObject();
    }

    if (!has_id && !has_detail && !has_func) {
        gjs_throw(cx, "Must specify at least one of signalId, detail, or func");
        return false;
    }

    *mask_out = GSignalMatchType(mask);
    if (has_id)
        *signal_id_out = signal_id;
    if (has_detail)
        *detail_out = detail;
    if (has_func)
        callable_out.set(callable);
    return true;
}

bool ObjectBase::signal_find(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
    if (!priv->check_is_instance(cx, "find signal"))
        return false;

    return priv->to_instance()->signal_find_impl(cx, args);
}

bool ObjectInstance::signal_find_impl(JSContext* cx, const JS::CallArgs& args) {
    gjs_debug_gsignal("[Gi.signal_find_symbol]() obj %p priv %p argc %d",
                      m_wrapper.get(), this, args.length());

    if (!check_gobject_finalized("find any signal on")) {
        args.rval().setInt32(0);
        return true;
    }

    JS::RootedObject match(cx);
    if (!gjs_parse_call_args(cx, "[Gi.signal_find_symbol]", args, "o", "match",
                             &match))
        return false;

    GSignalMatchType mask;
    unsigned signal_id;
    GQuark detail;
    JS::RootedObject callable(cx);
    if (!signal_match_arguments_from_object(cx, match, &mask, &signal_id,
                                            &detail, &callable))
        return false;

    uint64_t handler = 0;
    if (!callable) {
        handler = g_signal_handler_find(m_ptr, mask, signal_id, detail, nullptr,
                                        nullptr, nullptr);
    } else {
        for (GClosure* candidate : m_closures) {
            if (Gjs::Closure::for_gclosure(candidate)->callable() == callable) {
                handler = g_signal_handler_find(m_ptr, mask, signal_id, detail,
                                                candidate, nullptr, nullptr);
                if (handler != 0)
                    break;
            }
        }
    }

    args.rval().setNumber(static_cast<double>(handler));
    return true;
}

template <ObjectBase::SignalMatchFunc(*MatchFunc)>
static inline const char* signal_match_to_action_name();

template <>
inline const char*
signal_match_to_action_name<&g_signal_handlers_block_matched>() {
    return "block";
}

template <>
inline const char*
signal_match_to_action_name<&g_signal_handlers_unblock_matched>() {
    return "unblock";
}

template <>
inline const char*
signal_match_to_action_name<&g_signal_handlers_disconnect_matched>() {
    return "disconnect";
}

template <ObjectBase::SignalMatchFunc(*MatchFunc)>
bool ObjectBase::signals_action(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
    const std::string action_name = signal_match_to_action_name<MatchFunc>();
    if (!priv->check_is_instance(cx, (action_name + " signal").c_str()))
        return false;

    return priv->to_instance()->signals_action_impl<MatchFunc>(cx, args);
}

template <ObjectBase::SignalMatchFunc(*MatchFunc)>
bool ObjectInstance::signals_action_impl(JSContext* cx,
                                         const JS::CallArgs& args) {
    const std::string action_name = signal_match_to_action_name<MatchFunc>();
    const std::string action_tag = "[Gi.signals_" + action_name + "_symbol]";
    gjs_debug_gsignal("[%s]() obj %p priv %p argc %d", action_tag.c_str(),
                      m_wrapper.get(), this, args.length());

    if (!check_gobject_finalized((action_name + " any signal on").c_str())) {
        args.rval().setInt32(0);
        return true;
    }
    JS::RootedObject match(cx);
    if (!gjs_parse_call_args(cx, action_tag.c_str(), args, "o", "match",
                             &match)) {
        return false;
    }
    GSignalMatchType mask;
    unsigned signal_id;
    GQuark detail;
    JS::RootedObject callable(cx);
    if (!signal_match_arguments_from_object(cx, match, &mask, &signal_id,
                                            &detail, &callable)) {
        return false;
    }
    unsigned n_matched = 0;
    if (!callable) {
        n_matched = MatchFunc(m_ptr, mask, signal_id, detail, nullptr, nullptr,
                              nullptr);
    } else {
        std::vector<GClosure*> candidates;
        for (GClosure* candidate : m_closures) {
            if (Gjs::Closure::for_gclosure(candidate)->callable() == callable)
                candidates.push_back(candidate);
        }
        for (GClosure* candidate : candidates) {
            n_matched += MatchFunc(m_ptr, mask, signal_id, detail, candidate,
                                   nullptr, nullptr);
        }
    }

    args.rval().setNumber(n_matched);
    return true;
}

bool ObjectBase::to_string(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
    const char* kind = ObjectBase::DEBUG_TAG;
    if (!priv->is_prototype())
        kind = priv->to_instance()->to_string_kind();
    return gjs_wrapper_to_string_func(
        cx, obj, kind, priv->info(), priv->gtype(),
        priv->is_prototype() ? nullptr : priv->to_instance()->ptr(),
        args.rval());
}

/*
 * ObjectInstance::to_string_kind:
 *
 * ObjectInstance shows a "disposed" marker in its toString() method if the
 * wrapped GObject has already been disposed.
 */
const char* ObjectInstance::to_string_kind(void) const {
    if (m_gobj_finalized)
        return "object (FINALIZED)";
    return m_gobj_disposed ? "object (DISPOSED)" : "object";
}

/*
 * 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_CHECK_WRAPPER_PRIV(context, argc, vp, argv, obj, ObjectBase, priv);
    if (!priv->check_is_instance(context, "initialize"))
        return false;

    std::string dynamicString = priv->format_name() + "._init";
    AutoProfilerLabel label(context, "", dynamicString.c_str());

    return priv->to_instance()->init_impl(context, argv, obj);
}

// clang-format off
const struct JSClassOps ObjectBase::class_ops = {
    &ObjectBase::add_property,
    nullptr,  // deleteProperty
    nullptr,  // enumerate
    &ObjectBase::new_enumerate,
    &ObjectBase::resolve,
    nullptr,  // mayResolve
    &ObjectBase::finalize,
    NULL,
    NULL,
    &ObjectBase::trace,
};

const struct JSClass ObjectBase::klass = {
    "GObject_Object",
    JSCLASS_HAS_RESERVED_SLOTS(1) | JSCLASS_FOREGROUND_FINALIZE,
    &ObjectBase::class_ops
};

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_FS_END
};

JSPropertySpec ObjectBase::proto_properties[] = {
    JS_STRING_SYM_PS(toStringTag, "GObject_Object", JSPROP_READONLY),
    JS_PS_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(nullptr);
        return true;
    }

    JSObject* prototype = gjs_lookup_object_prototype(cx, parent_type);
    if (!prototype)
        return false;

    proto.set(prototype);
    return true;
}

bool ObjectPrototype::get_parent_constructor(
    JSContext* cx, JS::MutableHandleObject constructor) const {
    GType parent_type = g_type_parent(gtype());

    if (parent_type == G_TYPE_INVALID) {
        constructor.set(nullptr);
        return true;
    }

    JS::RootedValue v_constructor(cx);
    if (!gjs_lookup_object_constructor(cx, parent_type, &v_constructor))
        return false;

    g_assert(v_constructor.isObject() &&
             "gjs_lookup_object_constructor() should always produce an object");
    constructor.set(&v_constructor.toObject());
    return true;
}

void ObjectPrototype::set_interfaces(GType* interface_gtypes,
                                     uint32_t n_interface_gtypes) {
    if (interface_gtypes) {
        for (uint32_t n = 0; n < n_interface_gtypes; n++) {
            m_interface_gtypes.push_back(interface_gtypes[n]);
        }
    }
}

/*
 * ObjectPrototype::define_class:
 * @in_object: Object where the constructor is stored, typically a repo object.
 * @info: Introspection info for the GObject class.
 * @gtype: #GType for the GObject class.
 * @constructor: Return location for the constructor object.
 * @prototype: Return location for the prototype object.
 *
 * Define a GObject class constructor and prototype, including all the
 * necessary methods and properties that are not introspected. Provides the
 * constructor and prototype objects as out parameters, for convenience
 * elsewhere.
 */
bool ObjectPrototype::define_class(
    JSContext* context, JS::HandleObject in_object, GIObjectInfo* info,
    GType gtype, GType* interface_gtypes, uint32_t n_interface_gtypes,
    JS::MutableHandleObject constructor, JS::MutableHandleObject prototype) {
    ObjectPrototype* priv = ObjectPrototype::create_class(
        context, in_object, info, gtype, constructor, prototype);
    if (!priv)
        return false;

    priv->set_interfaces(interface_gtypes, n_interface_gtypes);

    JS::RootedObject parent_constructor(context);
    if (!priv->get_parent_constructor(context, &parent_constructor))
        return false;
    // If this is a fundamental constructor (e.g. GObject.Object) the
    // parent constructor may be null.
    if (parent_constructor) {
        if (!JS_SetPrototype(context, constructor, parent_constructor))
            return false;
    }

    // hook_up_vfunc and the signal handler matcher functions can't be included
    // in gjs_object_instance_proto_funcs because they are custom symbols.
    const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
    return JS_DefineFunctionById(context, prototype, atoms.hook_up_vfunc(),
                                 &ObjectBase::hook_up_vfunc, 3,
                                 GJS_MODULE_PROP_FLAGS) &&
           JS_DefineFunctionById(context, prototype, atoms.signal_find(),
                                 &ObjectBase::signal_find, 1,
                                 GJS_MODULE_PROP_FLAGS) &&
           JS_DefineFunctionById(
               context, prototype, atoms.signals_block(),
               &ObjectBase::signals_action<&g_signal_handlers_block_matched>, 1,
               GJS_MODULE_PROP_FLAGS) &&
           JS_DefineFunctionById(
               context, prototype, atoms.signals_unblock(),
               &ObjectBase::signals_action<&g_signal_handlers_unblock_matched>,
               1, GJS_MODULE_PROP_FLAGS) &&
           JS_DefineFunctionById(context, prototype, atoms.signals_disconnect(),
                                 &ObjectBase::signals_action<
                                     &g_signal_handlers_disconnect_matched>,
                                 1, GJS_MODULE_PROP_FLAGS);
}

/*
 * ObjectInstance::init_custom_class_from_gobject:
 *
 * Does all the necessary initialization for an ObjectInstance and JSObject
 * wrapper, given a newly-created GObject pointer, of a GObject class that was
 * created in JS with GObject.registerClass(). This is called from the GObject's
 * instance init function in gobject.cpp, and that's the only reason it's a
 * public method.
 */
bool ObjectInstance::init_custom_class_from_gobject(JSContext* cx,
                                                    JS::HandleObject wrapper,
                                                    GObject* gobj) {
    associate_js_gobject(cx, wrapper, gobj);

    // Custom JS objects will most likely have visible state, so just do this
    // from the start.
    if (!ensure_uses_toggle_ref(cx) || !m_uses_toggle_ref) {
        gjs_throw(cx, "Impossible to set toggle references on %sobject %p",
                  m_gobj_disposed ? "disposed " : "", gobj);
        return false;
    }

    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
    JS::RootedValue v(cx);
    if (!JS_GetPropertyById(cx, wrapper, atoms.instance_init(), &v))
        return false;

    if (v.isUndefined())
        return true;
    if (!v.isObject() || !JS::IsCallable(&v.toObject())) {
        gjs_throw(cx, "_instance_init property was not a function");
        return false;
    }

    JS::RootedValue ignored_rval(cx);
    return JS_CallFunctionValue(cx, wrapper, v, JS::HandleValueArray::empty(),
                                &ignored_rval);
}

/*
 * ObjectInstance::new_for_gobject:
 *
 * Creates a new JSObject wrapper for the GObject pointer @gobj, and an
 * ObjectInstance private structure to go along with it.
 */
ObjectInstance* ObjectInstance::new_for_gobject(JSContext* cx, GObject* gobj) {
    g_assert(gobj && "Cannot create JSObject for null GObject pointer");

    GType gtype = G_TYPE_FROM_INSTANCE(gobj);

    gjs_debug_marshal(GJS_DEBUG_GOBJECT, "Wrapping %s %p with JSObject",
                      g_type_name(gtype), gobj);

    JS::RootedObject proto(cx, gjs_lookup_object_prototype(cx, gtype));
    if (!proto)
        return nullptr;

    JS::RootedObject obj(
        cx, JS_NewObjectWithGivenProto(cx, &ObjectBase::klass, proto));
    if (!obj)
        return nullptr;

    ObjectPrototype* prototype = resolve_prototype(cx, proto);
    if (!prototype)
        return nullptr;

    ObjectInstance* priv = new ObjectInstance(prototype, obj);

    ObjectBase::init_private(obj, priv);

    g_object_ref_sink(gobj);
    priv->associate_js_gobject(cx, obj, gobj);

    g_assert(priv->wrapper() == obj.get());

    return priv;
}

/*
 * ObjectInstance::wrapper_from_gobject:
 *
 * Gets a JSObject wrapper for the GObject pointer @gobj. If one already exists,
 * then it is returned. Otherwise a new one is created with
 * ObjectInstance::new_for_gobject().
 */
JSObject* ObjectInstance::wrapper_from_gobject(JSContext* cx, GObject* gobj) {
    g_assert(gobj && "Cannot get JSObject for null GObject pointer");

    ObjectInstance* priv = ObjectInstance::for_gobject(gobj);

    if (!priv) {
        /* We have to create a wrapper */
        priv = new_for_gobject(cx, gobj);
        if (!priv)
            return nullptr;
    }

    return priv->wrapper();
}

bool ObjectInstance::set_value_from_gobject(JSContext* cx, GObject* gobj,
                                            JS::MutableHandleValue value_p) {
    if (!gobj) {
        value_p.setNull();
        return true;
    }

    auto* wrapper = ObjectInstance::wrapper_from_gobject(cx, gobj);
    if (wrapper) {
        value_p.setObject(*wrapper);
        return true;
    }

    gjs_throw(cx, "Failed to find JS object for GObject %p of type %s", gobj,
              g_type_name(G_TYPE_FROM_INSTANCE(gobj)));
    return false;
}

// Replaces GIWrapperBase::to_c_ptr(). The GIWrapperBase version is deleted.
bool ObjectBase::to_c_ptr(JSContext* cx, JS::HandleObject obj, GObject** ptr) {
    g_assert(ptr);

    auto* priv = ObjectBase::for_js(cx, obj);
    if (!priv || priv->is_prototype())
        return false;

    ObjectInstance* instance = priv->to_instance();
    if (!instance->check_gobject_finalized("access")) {
        *ptr = nullptr;
        return true;
    }

    *ptr = instance->ptr();
    return true;
}

// Overrides GIWrapperBase::transfer_to_gi_argument().
bool ObjectBase::transfer_to_gi_argument(JSContext* cx, JS::HandleObject obj,
                                         GIArgument* arg,
                                         GIDirection transfer_direction,
                                         GITransfer transfer_ownership,
                                         GType expected_gtype,
                                         GIBaseInfo* expected_info) {
    g_assert(transfer_direction != GI_DIRECTION_INOUT &&
             "transfer_to_gi_argument() must choose between in or out");

    if (!ObjectBase::typecheck(cx, obj, expected_info, expected_gtype)) {
        gjs_arg_unset<void*>(arg);
        return false;
    }

    GObject* ptr;
    if (!ObjectBase::to_c_ptr(cx, obj, &ptr))
        return false;

    gjs_arg_set(arg, ptr);

    // Pointer can be null if object was already disposed by C code
    if (!ptr)
        return true;

    if ((transfer_direction == GI_DIRECTION_IN &&
         transfer_ownership != GI_TRANSFER_NOTHING) ||
        (transfer_direction == GI_DIRECTION_OUT &&
         transfer_ownership == GI_TRANSFER_EVERYTHING)) {
        gjs_arg_set(arg, ObjectInstance::copy_ptr(cx, expected_gtype,
                                                  gjs_arg_get<void*>(arg)));
        if (!gjs_arg_get<void*>(arg))
            return false;
    }

    return true;
}

// Overrides GIWrapperInstance::typecheck_impl()
bool ObjectInstance::typecheck_impl(JSContext* cx, GIBaseInfo* expected_info,
                                    GType expected_type) const {
    g_assert(m_gobj_disposed || !m_ptr ||
             gtype() == G_OBJECT_TYPE(m_ptr.as<GObject*>()));
    return GIWrapperInstance::typecheck_impl(cx, expected_info, expected_type);
}

GJS_JSAPI_RETURN_CONVENTION
static bool find_vfunc_info(JSContext* context, GType implementor_gtype,
                            GIBaseInfo* vfunc_info, const char* vfunc_name,
                            void** implementor_vtable_ret,
                            GjsAutoFieldInfo* field_info_ret) {
    GType ancestor_gtype;
    int length, i;
    GIBaseInfo *ancestor_info;
    GjsAutoStructInfo struct_info;
    bool is_interface;

    field_info_ret->reset();
    *implementor_vtable_ret = NULL;

    ancestor_info = g_base_info_get_container(vfunc_info);
    ancestor_gtype = g_registered_type_info_get_g_type((GIRegisteredTypeInfo*)ancestor_info);

    is_interface = g_base_info_get_type(ancestor_info) == GI_INFO_TYPE_INTERFACE;

    GjsAutoTypeClass<GTypeClass> implementor_class(implementor_gtype);
    if (is_interface) {
        GTypeInstance *implementor_iface_class;
        implementor_iface_class = (GTypeInstance*) g_type_interface_peek(implementor_class,
                                                        ancestor_gtype);
        if (implementor_iface_class == NULL) {
            gjs_throw (context, "Couldn't find GType of implementor of interface %s.",
                       g_type_name(ancestor_gtype));
            return false;
        }

        *implementor_vtable_ret = implementor_iface_class;

        struct_info = g_interface_info_get_iface_struct((GIInterfaceInfo*)ancestor_info);
    } else {
        struct_info = g_object_info_get_class_struct((GIObjectInfo*)ancestor_info);
        *implementor_vtable_ret = implementor_class;
    }

    length = g_struct_info_get_n_fields(struct_info);
    for (i = 0; i < length; i++) {
        GjsAutoFieldInfo field_info = g_struct_info_get_field(struct_info, i);
        if (strcmp(field_info.name(), vfunc_name) != 0)
            continue;

        GjsAutoTypeInfo type_info = g_field_info_get_type(field_info);
        if (g_type_info_get_tag(type_info) != GI_TYPE_TAG_INTERFACE) {
            /* We have a field with the same name, but it's not a callback.
             * There's no hope of being another field with a correct name,
             * so just abort early. */
            return true;
        } else {
            *field_info_ret = std::move(field_info);
            return true;
        }
    }
    return true;
}

bool ObjectBase::hook_up_vfunc(JSContext* cx, unsigned argc, JS::Value* vp) {
    GJS_CHECK_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);
}

bool ObjectPrototype::hook_up_vfunc_impl(JSContext* cx,
                                         const JS::CallArgs& args) {
    JS::UniqueChars name;
    JS::RootedObject callable(cx);
    if (!gjs_parse_call_args(cx, "hook_up_vfunc", args, "so", "name", &name,
                             "callable", &callable))
        return false;

    args.rval().setUndefined();

    /* find the first class that actually has repository information */
    GIObjectInfo *info = m_info;
    GType info_gtype = m_gtype;
    while (!info && info_gtype != G_TYPE_OBJECT) {
        info_gtype = g_type_parent(info_gtype);

        info = g_irepository_find_by_gtype(nullptr, info_gtype);
    }

    /* If we don't have 'info', we don't have the base class (GObject).
     * This is awful, so abort now. */
    g_assert(info != NULL);

    GjsAutoVFuncInfo vfunc = g_object_info_find_vfunc(info, name.get());
    // Search the parent type chain
    while (!vfunc && info_gtype != G_TYPE_OBJECT) {
        info_gtype = g_type_parent(info_gtype);

        info = g_irepository_find_by_gtype(nullptr, info_gtype);
        if (info)
            vfunc = g_object_info_find_vfunc(info, name.get());
    }

    // If the vfunc doesn't exist in the parent
    // type chain, loop through the explicitly
    // defined interfaces...
    if (!vfunc) {
        for (GType interface_gtype : m_interface_gtypes) {
            GjsAutoInterfaceInfo interface =
                g_irepository_find_by_gtype(nullptr, interface_gtype);

            // Private and dynamic interfaces (defined in JS) do not have type
            // info.
            if (interface) {
                vfunc = g_interface_info_find_vfunc(interface, name.get());

                if (vfunc)
                    break;
            }
        }
    }

    // If the vfunc is still not found, it could exist on an interface
    // implemented by a parent. This is an error, as hooking up the vfunc
    // would create an implementation on the interface itself. In this
    // case, print a more helpful error than...
    // "Could not find definition of virtual function"
    //
    // See https://gitlab.gnome.org/GNOME/gjs/-/issues/89
    if (!vfunc) {
        unsigned n_interfaces;
        GjsAutoPointer<GType> interface_list =
            g_type_interfaces(m_gtype, &n_interfaces);

        for (unsigned i = 0; i < n_interfaces; i++) {
            GjsAutoInterfaceInfo interface =
                g_irepository_find_by_gtype(nullptr, interface_list[i]);

            if (interface) {
                GjsAutoVFuncInfo parent_vfunc =
                    g_interface_info_find_vfunc(interface, name.get());

                if (parent_vfunc) {
                    GjsAutoChar identifier = g_strdup_printf(
                        "%s.%s", interface.ns(), interface.name());
                    gjs_throw(cx,
                              "%s does not implement %s, add %s to your "
                              "implements array",
                              g_type_name(m_gtype), identifier.get(),
                              identifier.get());
                    return false;
                }
            }
        }

        // Fall back to less helpful error message
        gjs_throw(cx, "Could not find definition of virtual function %s",
                  name.get());
        return false;
    }

    void *implementor_vtable;
    GjsAutoFieldInfo field_info;
    if (!find_vfunc_info(cx, m_gtype, vfunc, name.get(), &implementor_vtable,
                         &field_info))
        return false;

    if (field_info) {
        gint offset;
        void* method_ptr;
        GjsCallbackTrampoline *trampoline;

        offset = g_field_info_get_offset(field_info);
        method_ptr = G_STRUCT_MEMBER_P(implementor_vtable, offset);

        if (!JS::IsCallable(callable)) {
            gjs_throw(cx, "Tried to deal with a vfunc that wasn't callable");
            return false;
        }
        trampoline = GjsCallbackTrampoline::create(
            cx, callable, vfunc, GI_SCOPE_TYPE_NOTIFIED, true, true);
        if (!trampoline)
            return false;

        // This is traced, and will be cleared from the list when the closure is
        // invalidated
        g_assert(std::find(m_vfuncs.begin(), m_vfuncs.end(), trampoline) ==
                     m_vfuncs.end() &&
                 "This vfunc was already associated with this class");
        m_vfuncs.push_back(trampoline);
        g_closure_add_invalidate_notifier(
            trampoline, this, &ObjectPrototype::vfunc_invalidated_notify);
        g_closure_add_invalidate_notifier(
            trampoline, nullptr,
            [](void*, GClosure* closure) { g_closure_unref(closure); });

        *reinterpret_cast<void**>(method_ptr) = trampoline->closure();
    }

    return true;
}

void ObjectPrototype::vfunc_invalidated_notify(void* data, GClosure* closure) {
    // This callback should *only* touch m_vfuncs
    auto* priv = static_cast<ObjectPrototype*>(data);
    Gjs::remove_one_from_unsorted_vector(&priv->m_vfuncs, closure);
}

bool
gjs_lookup_object_constructor(JSContext             *context,
                              GType                  gtype,
                              JS::MutableHandleValue value_p)
{
    JSObject *constructor;

    GjsAutoObjectInfo object_info = g_irepository_find_by_gtype(nullptr, gtype);

    constructor = gjs_lookup_object_constructor_from_info(context, object_info, gtype);

    if (G_UNLIKELY (constructor == NULL))
        return false;

    value_p.setObject(*constructor);
    return true;
}

void ObjectInstance::associate_string(GObject* obj, char* str) {
    auto* instance_strings = static_cast<GPtrArray*>(
        g_object_get_qdata(obj, ObjectBase::instance_strings_quark()));

    if (!instance_strings) {
        instance_strings = g_ptr_array_new_with_free_func(g_free);
        g_object_set_qdata_full(
            obj, ObjectBase::instance_strings_quark(), instance_strings,
            reinterpret_cast<GDestroyNotify>(g_ptr_array_unref));
    }
    g_ptr_array_add(instance_strings, str);
}
