[gjs: 1/12] js: Add new system for creating wrapper objects for native types




commit 30e89a8e7a67fb1c244ee2080fce5b3b2af4c1ab
Author: Philip Chimento <philip chimento gmail com>
Date:   Mon Nov 30 17:40:21 2020 -0800

    js: Add new system for creating wrapper objects for native types
    
    In order to phase out the GJS_DEFINE_PRIV_FROM_JS set of macros in
    jsapi-class.h, we introduce a new template class, CWrapper. This is like
    the GIWrapper family of classes in gi/wrapperutils.h, but without any
    usage of gobject-introspection. CWrapper simply implements a JS object
    that wraps a C pointer and stores its prototype in a private slot on the
    global object. It will be used for GI namespace objects, GI function
    objects, legacy importer objects, GType objects, and Cairo objects.
    
    CWrapper does most of the things that the macros did already, but with
    more type safety.
    
    In CWrapper we avoid using JS_InitClass() which the SpiderMonkey
    developers consider broken for various reasons. We use js::ClassSpec
    instead. The main change is that we now avoid the weird behaviour of
    JS_InitClass() where the constructor is always defined as a global object
    and the even weirder behaviour that if there is no constructor, then the
    prototype is defined as a global object. I don't believe this happened
    every time anyway, since there wasn't always a global object passed in,
    and I don't believe this will break any code in practice, but if it does,
    we can also add that behaviour to this template class.
    
    In addition, functionality (for_js(), for_js_nocheck(), for_js_typecheck()
    and typecheck()) that is common to both CWrapper and GIWrapperBase gets
    moved into a separate CRTP class, CWrapperPointerOps. We do have to give
    GIWrapperBase::for_js_typecheck() a slightly different signature than it
    previously had, because in this more general method we need to distinguish
    a null pointer from the error state.

 gi/boxed.cpp        |   6 +-
 gi/boxed.h          |   2 +
 gi/cwrapper.cpp     |  28 +++
 gi/cwrapper.h       | 504 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 gi/fundamental.cpp  |   5 +-
 gi/fundamental.h    |   2 +
 gi/gerror.cpp       |   8 +-
 gi/gerror.h         |   2 +
 gi/interface.cpp    |   4 +-
 gi/interface.h      |   2 +
 gi/object.cpp       |   1 +
 gi/private.cpp      |   4 +-
 gi/union.h          |   2 +
 gi/wrapperutils.cpp |  16 --
 gi/wrapperutils.h   | 102 ++---------
 meson.build         |   1 +
 16 files changed, 567 insertions(+), 122 deletions(-)
---
diff --git a/gi/boxed.cpp b/gi/boxed.cpp
index 49c999ee..8aebd4f3 100644
--- a/gi/boxed.cpp
+++ b/gi/boxed.cpp
@@ -596,11 +596,7 @@ bool BoxedInstance::set_nested_interface_object(JSContext* context,
         args[0].set(value);
         JS::RootedObject tmp_object(context,
             gjs_construct_object_dynamic(context, proto, args));
-        if (!tmp_object)
-            return false;
-
-        source_priv = BoxedBase::for_js_typecheck(context, tmp_object);
-        if (!source_priv)
+        if (!tmp_object || !for_js_typecheck(context, tmp_object, &source_priv))
             return false;
     }
 
diff --git a/gi/boxed.h b/gi/boxed.h
index 2fd05ab7..7c1cd49c 100644
--- a/gi/boxed.h
+++ b/gi/boxed.h
@@ -21,6 +21,7 @@
 #include <js/RootingAPI.h>
 #include <js/TypeDecls.h>
 
+#include "gi/cwrapper.h"
 #include "gi/wrapperutils.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/macros.h"
@@ -44,6 +45,7 @@ class SystemAllocPolicy;
 
 class BoxedBase
     : public GIWrapperBase<BoxedBase, BoxedPrototype, BoxedInstance> {
+    friend class CWrapperPointerOps<BoxedBase>;
     friend class GIWrapperBase<BoxedBase, BoxedPrototype, BoxedInstance>;
 
  protected:
diff --git a/gi/cwrapper.cpp b/gi/cwrapper.cpp
new file mode 100644
index 00000000..f5fbff8a
--- /dev/null
+++ b/gi/cwrapper.cpp
@@ -0,0 +1,28 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2012 Red Hat, Inc.
+
+#include <config.h>
+
+#include <glib-object.h>
+
+#include <js/PropertyDescriptor.h>  // for JSPROP_PERMANENT
+#include <js/RootingAPI.h>
+#include <js/TypeDecls.h>
+#include <jsapi.h>  // for JS_DefinePropertyById
+
+#include "gi/cwrapper.h"
+#include "gi/gtype.h"
+#include "gjs/atoms.h"
+#include "gjs/context-private.h"
+
+bool gjs_wrapper_define_gtype_prop(JSContext* cx, JS::HandleObject constructor,
+                                   GType gtype) {
+    JS::RootedObject gtype_obj(cx, gjs_gtype_create_gtype_wrapper(cx, gtype));
+    if (!gtype_obj)
+        return false;
+
+    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+    return JS_DefinePropertyById(cx, constructor, atoms.gtype(), gtype_obj,
+                                 JSPROP_PERMANENT);
+}
diff --git a/gi/cwrapper.h b/gi/cwrapper.h
new file mode 100644
index 00000000..19bcc6ff
--- /dev/null
+++ b/gi/cwrapper.h
@@ -0,0 +1,504 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Philip Chimento <philip chimento gmail com>
+
+#pragma once
+
+#include <config.h>
+
+#include <assert.h>
+
+#include <string>
+#include <type_traits>  // for integral_constant
+
+#include <glib-object.h>  // for GType
+
+#include <js/CallArgs.h>
+#include <js/Class.h>
+#include <js/ComparisonOperators.h>
+#include <js/Id.h>
+#include <js/RootingAPI.h>
+#include <js/TypeDecls.h>
+#include <js/Value.h>
+#include <jsapi.h>
+#include <jspubtd.h>
+
+#include "gjs/jsapi-util.h"
+#include "gjs/macros.h"
+#include "util/log.h"
+
+struct JSFunctionSpec;
+struct JSPropertySpec;
+
+// gi/cwrapper.h - template implementing a JS object that wraps a C pointer.
+// This template is used for many of the special objects in GJS. It contains
+// functionality such as storing the class's prototype in a global slot, where
+// it can be easily retrieved in order to create new objects.
+
+/*
+ * GJS_GET_WRAPPER_PRIV:
+ * @cx: JSContext pointer passed into JSNative function
+ * @argc: Number of arguments passed into JSNative function
+ * @vp: Argument value array passed into JSNative function
+ * @args: Name for JS::CallArgs variable defined by this code snippet
+ * @thisobj: Name for JS::RootedObject variable referring to function's this
+ * @type: Type of private data
+ * @priv: Name for private data variable defined by this code snippet
+ *
+ * A convenience macro for getting the private data from GJS classes using
+ * CWrapper or GIWrapper.
+ * Throws an error and returns false if the 'this' object is not the right type.
+ * Use in any JSNative function.
+ */
+#define GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, thisobj, type, priv) \
+    GJS_GET_THIS(cx, argc, vp, args, thisobj);                        \
+    type* priv;                                                       \
+    if (!type::for_js_typecheck(cx, thisobj, &priv, &args))           \
+        return false;
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_wrapper_define_gtype_prop(JSContext* cx, JS::HandleObject constructor,
+                                   GType gtype);
+
+/*
+ * CWrapperPointerOps:
+ *
+ * This class contains methods that are common to both CWrapper and
+ * GIWrapperBase, for retrieving the wrapped C pointer out of the JS object.
+ */
+template <class Base, typename Wrapped = Base>
+class CWrapperPointerOps {
+ public:
+    /*
+     * CWrapperPointerOps::for_js:
+     *
+     * Gets the wrapped C pointer belonging to a particular JS object wrapper.
+     * Checks that the wrapper object has the right JSClass (Base::klass).
+     * A null return value means either that the object didn't have the right
+     * class, or that no private data has been set yet on the wrapper. To
+     * distinguish between these two cases, use for_js_typecheck().
+     */
+    [[nodiscard]] static Wrapped* for_js(JSContext* cx,
+                                         JS::HandleObject wrapper) {
+        return static_cast<Wrapped*>(
+            JS_GetInstancePrivate(cx, wrapper, &Base::klass, nullptr));
+    }
+
+    /*
+     * CWrapperPointerOps::typecheck:
+     *
+     * Checks if the given wrapper object has the right JSClass (Base::klass).
+     */
+    [[nodiscard]] static bool typecheck(JSContext* cx, JS::HandleObject wrapper,
+                                        JS::CallArgs* args = nullptr) {
+        return JS_InstanceOf(cx, wrapper, &Base::klass, args);
+    }
+
+    /*
+     * CWrapperPointerOps::for_js_typecheck:
+     *
+     * Like for_js(), only throws a JS exception if the wrapper object has the
+     * wrong class. Use in JSNative functions, where you have access to a
+     * JS::CallArgs. The exception message will mention args.callee.
+     *
+     * The second overload can be used when you don't have access to an
+     * instance of JS::CallArgs. The exception message will be generic.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool for_js_typecheck(JSContext* cx, JS::HandleObject wrapper,
+                                 Wrapped** out, JS::CallArgs* args) {
+        if (!typecheck(cx, wrapper, args))
+            return false;
+        *out = for_js_nocheck(wrapper);
+        return true;
+    }
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool for_js_typecheck(JSContext* cx, JS::HandleObject wrapper,
+                                 Wrapped** out) {
+        if (!typecheck(cx, wrapper)) {
+            const JSClass* obj_class = JS_GetClass(wrapper);
+            gjs_throw_custom(cx, JSProto_TypeError, nullptr,
+                             "Object %p is not a subclass of %s, it's a %s",
+                             wrapper.get(), Base::klass.name, obj_class->name);
+            return false;
+        }
+        *out = for_js_nocheck(wrapper);
+        return true;
+    }
+
+    /*
+     * CWrapperPointerOps::for_js_nocheck:
+     *
+     * Use when you don't have a JSContext* available. This method is infallible
+     * and cannot trigger a GC, so it's safe to use from finalize() and trace().
+     * (It can return null if no private data has been set yet on the wrapper.)
+     */
+    [[nodiscard]] static Wrapped* for_js_nocheck(JSObject* wrapper) {
+        return static_cast<Wrapped*>(JS_GetPrivate(wrapper));
+    }
+};
+
+/*
+ * CWrapper:
+ *
+ * This template implements a JS object that wraps a C pointer, stores its
+ * prototype in a global slot, and includes some optional functionality.
+ *
+ * If you derive from this class, you must implement:
+ *  - static constexpr GjsGlobalSlot PROTOTYPE_SLOT: global slot that the
+ *    prototype will be stored in
+ *  - static constexpr GjsDebugTopic DEBUG_TOPIC: debug log domain
+ *  - static constexpr JSClass klass: see documentation in SpiderMonkey; the
+ *    class may have JSClassOps (see below under CWrapper::class_ops) but must
+ *    at least have its js::ClassSpec member set. The members of js::ClassSpec
+ *    are createConstructor, createPrototype, constructorFunctions,
+ *    constructorProperties, prototypeFunctions, prototypeProperties,
+ *    finishInit, and flags.
+ *  - static Wrapped* constructor_impl(JSContext*, const JS::CallArgs&): custom
+ *    constructor functionality. If your JS object doesn't need a constructor
+ *    (i.e. user code can't use the `new` operator on it) then you can skip this
+ *    one, and include js::ClassSpec::DontDefineConstructor in your
+ *    class_spec's flags member.
+ *  - static constexpr unsigned constructor_nargs: number of arguments that the
+ *    constructor takes. If you implement constructor_impl() then also add this.
+ *  - void finalize_impl(JSFreeOp*, Wrapped*): called when the JS object is
+ *    garbage collected, use this to free the C pointer and do any other cleanup
+ *
+ * Add optional functionality by setting members of class_spec:
+ *  - createConstructor: the default is to create a constructor function that
+ *    calls constructor_impl(), unless flags includes DontDefineConstructor. If
+ *    you need something else, set this member.
+ *  - createPrototype: the default is to use a plain object as the prototype. If
+ *    you need something else, set this member.
+ *  - constructorFunctions: If the class has static methods, set this member.
+ *  - constructorProperties: If the class has static properties, set this
+ *    member.
+ *  - prototypeFunctions: If the class has methods, set this member.
+ *  - prototypeProperties: If the class has properties, set this member.
+ *  - finishInit: If you need to do any other initialization on the prototype or
+ *    the constructor object, set this member.
+ *  - flags: Specify DontDefineConstructor here if you don't want a user-visible
+ *    constructor.
+ *
+ * You may override CWrapper::class_ops if you want to opt in to more JSClass
+ * operations. In that case, CWrapper includes some optional functionality:
+ *  - resolve: include &resolve in your class_ops, and implement
+ *    bool resolve_impl(JSContext*, JS::HandleObject, JS::HandleId, bool*).
+ *  - new enumerate: include &new_enumerate in your class_ops, and implement
+ *    bool new_enumerate_impl(JSContext*, JS::HandleObject,
+ *    JS::MutableHandleIdVector, bool).
+ *
+ * This template uses the Curiously Recurring Template Pattern (CRTP), which
+ * requires inheriting classes to declare themselves friends of the parent
+ * class, so that the parent class can call their private methods.
+ *
+ * For more information about the CRTP, the Wikipedia article is informative:
+ * https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
+ */
+template <class Base, typename Wrapped = Base>
+class CWrapper : public CWrapperPointerOps<Base, Wrapped> {
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool constructor(JSContext* cx, unsigned argc, JS::Value* vp) {
+        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+        if (!args.isConstructing()) {
+            gjs_throw_constructor_error(cx);
+            return false;
+        }
+        JS::RootedObject object(
+            cx, JS_NewObjectForConstructor(cx, &Base::klass, args));
+        if (!object)
+            return false;
+
+        Wrapped* priv = Base::constructor_impl(cx, args);
+        if (!priv)
+            return false;
+        JS_SetPrivate(object, priv);
+
+        args.rval().setObject(*object);
+        return true;
+    }
+
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool abstract_constructor(JSContext* cx, unsigned argc,
+                                     JS::Value* vp) {
+        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+        gjs_throw_abstract_constructor_error(cx, args);
+        return false;
+    }
+
+    // Debug methods, no-op unless verbose logging is compiled in
+
+ protected:
+    static void debug_lifecycle(
+        const void* wrapped_ptr GJS_USED_VERBOSE_LIFECYCLE,
+        const void* obj GJS_USED_VERBOSE_LIFECYCLE,
+        const char* message GJS_USED_VERBOSE_LIFECYCLE) {
+        gjs_debug_lifecycle(Base::DEBUG_TOPIC, "[%p: JS wrapper %p] %s",
+                            wrapped_ptr, obj, message);
+    }
+    void debug_jsprop(const char* message GJS_USED_VERBOSE_PROPS,
+                      const char* id GJS_USED_VERBOSE_PROPS,
+                      const void* obj GJS_USED_VERBOSE_PROPS) const {
+        gjs_debug_jsprop(Base::DEBUG_TOPIC, "[%p: JS wrapper %p] %s prop %s",
+                         this, obj, message, id);
+    }
+    void debug_jsprop(const char* message, jsid id, const void* obj) const {
+        if constexpr (GJS_VERBOSE_ENABLE_PROPS)
+            debug_jsprop(message, gjs_debug_id(id).c_str(), obj);
+    }
+
+    static void finalize(JSFreeOp* fop, JSObject* obj) {
+        Wrapped* priv = Base::for_js_nocheck(obj);
+
+        // Call only CWrapper's original method here, not any overrides; e.g.,
+        // we don't want to deal with a read barrier.
+        CWrapper::debug_lifecycle(priv, obj, "Finalize");
+
+        Base::finalize_impl(fop, priv);
+
+        // Remove the pointer from the JSObject
+        JS_SetPrivate(obj, nullptr);
+    }
+
+    static constexpr JSClassOps class_ops = {
+        nullptr,  // addProperty
+        nullptr,  // deleteProperty
+        nullptr,  // enumerate
+        nullptr,  // newEnumerate
+        nullptr,  // resolve
+        nullptr,  // mayResolve
+        &CWrapper::finalize,
+    };
+
+    /*
+     * CWrapper::create_abstract_constructor:
+     *
+     * This function can be used as the createConstructor member of class_ops.
+     * It creates a constructor that always throws if it is the new.target. Use
+     * it if you do need a constructor object to exist (for example, if it has
+     * static methods) but you don't want it to be able to be called.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static JSObject* create_abstract_constructor(JSContext* cx, JSProtoKey) {
+        return JS_GetFunctionObject(
+            JS_NewFunction(cx, &Base::abstract_constructor, 0,
+                           JSFUN_CONSTRUCTOR, Base::klass.name));
+    }
+
+    /*
+     * CWrapper::define_gtype_prop:
+     *
+     * This function can be used as the finishInit member of class_ops. It
+     * defines a '$gtype' property on the constructor. If you use it, you must
+     * implement a gtype() static method that returns the GType to define.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool define_gtype_prop(JSContext* cx, JS::HandleObject ctor,
+                                  JS::HandleObject proto [[maybe_unused]]) {
+        return gjs_wrapper_define_gtype_prop(cx, ctor, Base::gtype());
+    }
+
+    // Used to get the prototype when it is guaranteed to have already been
+    // created
+    GJS_JSAPI_RETURN_CONVENTION
+    static JSObject* prototype(JSContext* cx) {
+        JSObject* global = JS::CurrentGlobalOrNull(cx);
+        assert(global && "Must be in a realm to call prototype()");
+        JS::RootedValue v_proto(
+            cx, gjs_get_global_slot(global, Base::PROTOTYPE_SLOT));
+        assert(!v_proto.isUndefined() &&
+               "create_prototype() must be called before prototype()");
+        assert(v_proto.isObject() &&
+               "Someone stored some weird value in a global slot");
+        return &v_proto.toObject();
+    }
+
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id,
+                        bool* resolved) {
+        Wrapped* priv = CWrapperPointerOps<Base, Wrapped>::for_js(cx, obj);
+        assert(priv && "resolve called on wrong object");
+        priv->debug_jsprop("Resolve hook", id, obj);
+        return priv->resolve_impl(cx, obj, id, resolved);
+    }
+
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool new_enumerate(JSContext* cx, JS::HandleObject obj,
+                              JS::MutableHandleIdVector properties,
+                              bool only_enumerable) {
+        Wrapped* priv = CWrapperPointerOps<Base, Wrapped>::for_js(cx, obj);
+        assert(priv && "enumerate called on wrong object");
+        priv->debug_jsprop("Enumerate hook", "(all)", obj);
+        return priv->new_enumerate_impl(cx, obj, properties, only_enumerable);
+    }
+
+ public:
+    /*
+     * CWrapper::create_prototype:
+     * @module: Object on which to define the constructor as a property, or
+     *   the global object if not given
+     *
+     * Create the class's prototype and store it in the global slot, or
+     * retrieve it if it has already been created.
+     *
+     * Unless DontDefineConstructor is in class_ops.flags, also create the
+     * class's constructor, and define it as a property on @module.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static JSObject* create_prototype(JSContext* cx,
+                                      JS::HandleObject module = nullptr) {
+        JSObject* global = JS::CurrentGlobalOrNull(cx);
+        assert(global && "Must be in a realm to call create_prototype()");
+
+        // If we've been here more than once, we already have the proto
+        JS::RootedValue v_proto(
+            cx, gjs_get_global_slot(global, Base::PROTOTYPE_SLOT));
+        if (!v_proto.isUndefined()) {
+            assert(v_proto.isObject() &&
+                   "Someone stored some weird value in a global slot");
+            return &v_proto.toObject();
+        }
+
+        // Workaround for bogus warning
+        // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94554
+        // Note that the corresponding function pointers in the js::ClassSpec
+        // must be initialized as nullptr, not the default initializer! (see
+        // e.g. CairoPath::class_spec.finishInit)
+        using NullOpType =
+            std::integral_constant<js::ClassObjectCreationOp, nullptr>;
+        using CreateConstructorType =
+            std::integral_constant<js::ClassObjectCreationOp,
+                                   Base::klass.spec->createConstructor>;
+        using CreatePrototypeType =
+            std::integral_constant<js::ClassObjectCreationOp,
+                                   Base::klass.spec->createPrototype>;
+        using NullFuncsType =
+            std::integral_constant<const JSFunctionSpec*, nullptr>;
+        using ConstructorFuncsType =
+            std::integral_constant<const JSFunctionSpec*,
+                                   Base::klass.spec->constructorFunctions>;
+        using PrototypeFuncsType =
+            std::integral_constant<const JSFunctionSpec*,
+                                   Base::klass.spec->prototypeFunctions>;
+        using NullPropsType =
+            std::integral_constant<const JSPropertySpec*, nullptr>;
+        using ConstructorPropsType =
+            std::integral_constant<const JSPropertySpec*,
+                                   Base::klass.spec->constructorProperties>;
+        using PrototypePropsType =
+            std::integral_constant<const JSPropertySpec*,
+                                   Base::klass.spec->prototypeProperties>;
+        using NullFinishOpType =
+            std::integral_constant<js::FinishClassInitOp, nullptr>;
+        using FinishInitType =
+            std::integral_constant<js::FinishClassInitOp,
+                                   Base::klass.spec->finishInit>;
+
+        // Create the prototype. If no createPrototype function is provided,
+        // then the default is to create a plain object as the prototype.
+        JS::RootedObject proto(cx);
+        if constexpr (!std::is_same_v<CreatePrototypeType, NullOpType>) {
+            proto = Base::klass.spec->createPrototype(cx, JSProto_Object);
+        } else {
+            proto = JS_NewPlainObject(cx);
+        }
+        if (!proto)
+            return nullptr;
+
+        if constexpr (!std::is_same_v<PrototypePropsType, NullPropsType>) {
+            if (!JS_DefineProperties(cx, proto,
+                                     Base::klass.spec->prototypeProperties))
+                return nullptr;
+        }
+        if constexpr (!std::is_same_v<PrototypeFuncsType, NullFuncsType>) {
+            if (!JS_DefineFunctions(cx, proto,
+                                    Base::klass.spec->prototypeFunctions))
+                return nullptr;
+        }
+
+        gjs_set_global_slot(global, Base::PROTOTYPE_SLOT,
+                            JS::ObjectValue(*proto));
+
+        // Create the constructor. If no createConstructor function is provided,
+        // then the default is to call CWrapper::constructor() which calls
+        // Base::constructor_impl().
+        JS::RootedObject ctor_obj(cx);
+        if constexpr (!(Base::klass.spec->flags &
+                        js::ClassSpec::DontDefineConstructor)) {
+            if constexpr (!std::is_same_v<CreateConstructorType, NullOpType>) {
+                ctor_obj =
+                    Base::klass.spec->createConstructor(cx, JSProto_Object);
+            } else {
+                JSFunction* ctor = JS_NewFunction(
+                    cx, &Base::constructor, Base::constructor_nargs,
+                    JSFUN_CONSTRUCTOR, Base::klass.name);
+                ctor_obj = JS_GetFunctionObject(ctor);
+            }
+            if (!ctor_obj ||
+                !JS_LinkConstructorAndPrototype(cx, ctor_obj, proto))
+                return nullptr;
+            if constexpr (!std::is_same_v<ConstructorPropsType,
+                                          NullPropsType>) {
+                if (!JS_DefineProperties(
+                        cx, ctor_obj, Base::klass.spec->constructorProperties))
+                    return nullptr;
+            }
+            if constexpr (!std::is_same_v<ConstructorFuncsType,
+                                          NullFuncsType>) {
+                if (!JS_DefineFunctions(cx, ctor_obj,
+                                        Base::klass.spec->constructorFunctions))
+                    return nullptr;
+            }
+        }
+
+        if constexpr (!std::is_same_v<FinishInitType, NullFinishOpType>) {
+            if (!Base::klass.spec->finishInit(cx, ctor_obj, proto))
+                return nullptr;
+        }
+
+        // Put the constructor, if one exists, as a property on the module
+        // object. If module is not given, we are defining a global class.
+        if (ctor_obj) {
+            JS::RootedObject in_obj(cx, module);
+            if (!in_obj)
+                in_obj = gjs_get_import_global(cx);
+            JS::RootedId class_name(
+                cx, gjs_intern_string_to_id(cx, Base::klass.name));
+            if (class_name == JSID_VOID ||
+                !JS_DefinePropertyById(cx, in_obj, class_name, ctor_obj,
+                                       GJS_MODULE_PROP_FLAGS))
+                return nullptr;
+        }
+
+        gjs_debug(GJS_DEBUG_CONTEXT, "Initialized class %s prototype %p",
+                  Base::klass.name, proto.get());
+        return proto;
+    }
+
+    /*
+     * CWrapper::from_c_ptr():
+     *
+     * Create a new CWrapper JS object from the given C pointer. The pointer
+     * is copied using copy_ptr(), so you must implement that if you use this
+     * function.
+     */
+    GJS_JSAPI_RETURN_CONVENTION
+    static JSObject* from_c_ptr(JSContext* cx, Wrapped* ptr) {
+        JS::RootedObject proto(cx, Base::prototype(cx));
+        if (!proto)
+            return nullptr;
+
+        JS::RootedObject wrapper(
+            cx, JS_NewObjectWithGivenProto(cx, &Base::klass, proto));
+        if (!wrapper)
+            return nullptr;
+
+        assert(!JS_GetPrivate(wrapper));
+        JS_SetPrivate(wrapper, Base::copy_ptr(ptr));
+
+        debug_lifecycle(ptr, wrapper, "from_c_ptr");
+
+        return wrapper;
+    }
+};
diff --git a/gi/fundamental.cpp b/gi/fundamental.cpp
index c60d18f8..94519df9 100644
--- a/gi/fundamental.cpp
+++ b/gi/fundamental.cpp
@@ -456,8 +456,9 @@ JSObject* FundamentalInstance::object_for_gvalue(JSContext* cx,
 
 bool FundamentalBase::to_gvalue(JSContext* cx, JS::HandleObject obj,
                                 GValue* gvalue) {
-    auto* priv = FundamentalBase::for_js_typecheck(cx, obj);
-    if (!priv || !priv->check_is_instance(cx, "convert to GValue"))
+    FundamentalBase* priv;
+    if (!for_js_typecheck(cx, obj, &priv) ||
+        !priv->check_is_instance(cx, "convert to GValue"))
         return false;
 
     priv->to_instance()->set_value(gvalue);
diff --git a/gi/fundamental.h b/gi/fundamental.h
index aa8f0762..27afb1e2 100644
--- a/gi/fundamental.h
+++ b/gi/fundamental.h
@@ -13,6 +13,7 @@
 
 #include <js/TypeDecls.h>
 
+#include "gi/cwrapper.h"
 #include "gi/wrapperutils.h"
 #include "gjs/jsapi-util.h"  // for GjsAutoCallableInfo
 #include "gjs/macros.h"
@@ -31,6 +32,7 @@ namespace JS { class CallArgs; }
 class FundamentalBase
     : public GIWrapperBase<FundamentalBase, FundamentalPrototype,
                            FundamentalInstance> {
+    friend class CWrapperPointerOps<FundamentalBase>;
     friend class GIWrapperBase<FundamentalBase, FundamentalPrototype,
                                FundamentalInstance>;
 
diff --git a/gi/gerror.cpp b/gi/gerror.cpp
index 3d01390c..71dd6089 100644
--- a/gi/gerror.cpp
+++ b/gi/gerror.cpp
@@ -136,8 +136,8 @@ bool ErrorBase::to_string(JSContext* context, unsigned argc, JS::Value* vp) {
         return gjs_string_from_utf8(context, descr, rec.rval());
     }
 
-    ErrorBase* priv = ErrorBase::for_js_typecheck(context, self, rec);
-    if (!priv)
+    ErrorBase* priv;
+    if (!for_js_typecheck(context, self, &priv, &rec))
         return false;
 
     /* We follow the same pattern as standard JS errors, at the expense of
@@ -168,8 +168,8 @@ bool ErrorBase::value_of(JSContext* context, unsigned argc, JS::Value* vp) {
         return false;
     }
 
-    ErrorBase* priv = ErrorBase::for_js_typecheck(context, prototype, rec);
-    if (!priv)
+    ErrorBase* priv;
+    if (!for_js_typecheck(context, prototype, &priv, &rec))
         return false;
 
     rec.rval().setInt32(priv->domain());
diff --git a/gi/gerror.h b/gi/gerror.h
index e2d63107..f64112c0 100644
--- a/gi/gerror.h
+++ b/gi/gerror.h
@@ -14,6 +14,7 @@
 #include <js/PropertySpec.h>
 #include <js/TypeDecls.h>
 
+#include "gi/cwrapper.h"
 #include "gi/wrapperutils.h"
 #include "gjs/jsapi-util.h"  // for GjsAutoPointer operators
 #include "gjs/macros.h"
@@ -41,6 +42,7 @@ class CallArgs;
 
 class ErrorBase
     : public GIWrapperBase<ErrorBase, ErrorPrototype, ErrorInstance> {
+    friend class CWrapperPointerOps<ErrorBase>;
     friend class GIWrapperBase<ErrorBase, ErrorPrototype, ErrorInstance>;
 
  protected:
diff --git a/gi/interface.cpp b/gi/interface.cpp
index 23b64fab..925097e8 100644
--- a/gi/interface.cpp
+++ b/gi/interface.cpp
@@ -86,8 +86,8 @@ bool InterfaceBase::has_instance(JSContext* cx, unsigned argc, JS::Value* vp) {
                                      &interface_proto))
         return false;
 
-    InterfaceBase* priv = InterfaceBase::for_js_typecheck(cx, interface_proto);
-    if (!priv)
+    InterfaceBase* priv;
+    if (!for_js_typecheck(cx, interface_proto, &priv))
         return false;
 
     return priv->to_prototype()->has_instance_impl(cx, args);
diff --git a/gi/interface.h b/gi/interface.h
index b3ddaecf..490b7093 100644
--- a/gi/interface.h
+++ b/gi/interface.h
@@ -17,6 +17,7 @@
 #include <js/RootingAPI.h>
 #include <js/TypeDecls.h>
 
+#include "gi/cwrapper.h"
 #include "gi/wrapperutils.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/macros.h"
@@ -39,6 +40,7 @@ class InterfaceInstance;
 
 class InterfaceBase : public GIWrapperBase<InterfaceBase, InterfacePrototype,
                                            InterfaceInstance> {
+    friend class CWrapperPointerOps<InterfaceBase>;
     friend class GIWrapperBase<InterfaceBase, InterfacePrototype,
                                InterfaceInstance>;
 
diff --git a/gi/object.cpp b/gi/object.cpp
index f8323bb0..50765aea 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -40,6 +40,7 @@
 #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"
diff --git a/gi/private.cpp b/gi/private.cpp
index 86300c6e..935295bb 100644
--- a/gi/private.cpp
+++ b/gi/private.cpp
@@ -257,8 +257,8 @@ static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
 
     /* Don't pass the argv to it, as otherwise we will log about the callee
      * while we only care about the parent object type. */
-    auto* parent_priv = ObjectBase::for_js_typecheck(cx, parent);
-    if (!parent_priv)
+    ObjectBase* parent_priv;
+    if (!ObjectBase::for_js_typecheck(cx, parent, &parent_priv))
         return false;
 
     uint32_t n_interfaces, n_properties;
diff --git a/gi/union.h b/gi/union.h
index 71faf5ba..269cf156 100644
--- a/gi/union.h
+++ b/gi/union.h
@@ -12,6 +12,7 @@
 
 #include <js/TypeDecls.h>
 
+#include "gi/cwrapper.h"
 #include "gi/wrapperutils.h"
 #include "gjs/macros.h"
 #include "util/log.h"
@@ -26,6 +27,7 @@ class UnionInstance;
 
 class UnionBase
     : public GIWrapperBase<UnionBase, UnionPrototype, UnionInstance> {
+    friend class CWrapperPointerOps<UnionBase>;
     friend class GIWrapperBase<UnionBase, UnionPrototype, UnionInstance>;
 
  protected:
diff --git a/gi/wrapperutils.cpp b/gi/wrapperutils.cpp
index c314ffd0..88f8330c 100644
--- a/gi/wrapperutils.cpp
+++ b/gi/wrapperutils.cpp
@@ -9,15 +9,10 @@
 #include <girepository.h>
 #include <glib-object.h>
 
-#include <js/PropertyDescriptor.h>  // for JSPROP_PERMANENT
 #include <js/TypeDecls.h>
-#include <jsapi.h>  // for JS_DefinePropertyById
 
 #include "gi/function.h"
-#include "gi/gtype.h"
 #include "gi/wrapperutils.h"
-#include "gjs/atoms.h"
-#include "gjs/context-private.h"
 #include "gjs/jsapi-util.h"
 
 /* Default spidermonkey toString is worthless.  Replace it
@@ -64,17 +59,6 @@ bool gjs_wrapper_throw_readonly_field(JSContext* cx, GType gtype,
     return false;
 }
 
-bool gjs_wrapper_define_gtype_prop(JSContext* cx, JS::HandleObject constructor,
-                                   GType gtype) {
-    JS::RootedObject gtype_obj(cx, gjs_gtype_create_gtype_wrapper(cx, gtype));
-    if (!gtype_obj)
-        return false;
-
-    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
-    return JS_DefinePropertyById(cx, constructor, atoms.gtype(), gtype_obj,
-                                 JSPROP_PERMANENT);
-}
-
 // These policies work around having separate g_foo_info_get_n_methods() and
 // g_foo_info_get_method() functions for different GIInfoTypes. It's not
 // possible to use GIFooInfo* as the template parameter, because the GIFooInfo
diff --git a/gi/wrapperutils.h b/gi/wrapperutils.h
index df1e5c19..388e3999 100644
--- a/gi/wrapperutils.h
+++ b/gi/wrapperutils.h
@@ -26,6 +26,7 @@
 #include <jspubtd.h>     // for JSProto_TypeError
 
 #include "gi/arg-inl.h"
+#include "gi/cwrapper.h"
 #include "gjs/atoms.h"
 #include "gjs/context-private.h"
 #include "gjs/jsapi-class.h"  // IWYU pragma: keep
@@ -49,10 +50,6 @@ bool gjs_wrapper_throw_nonexistent_field(JSContext* cx, GType gtype,
 bool gjs_wrapper_throw_readonly_field(JSContext* cx, GType gtype,
                                       const char* field_name);
 
-GJS_JSAPI_RETURN_CONVENTION
-bool gjs_wrapper_define_gtype_prop(JSContext* cx, JS::HandleObject constructor,
-                                   GType gtype);
-
 namespace InfoType {
 enum Tag { Enum, Interface, Object, Struct, Union };
 }
@@ -73,27 +70,6 @@ template <InfoType::Tag>
 GJS_JSAPI_RETURN_CONVENTION bool gjs_define_static_methods(
     JSContext* cx, JS::HandleObject constructor, GType gtype, GIBaseInfo* info);
 
-/*
- * GJS_GET_WRAPPER_PRIV:
- * @cx: JSContext pointer passed into JSNative function
- * @argc: Number of arguments passed into JSNative function
- * @vp: Argument value array passed into JSNative function
- * @args: Name for JS::CallArgs variable defined by this code snippet
- * @thisobj: Name for JS::RootedObject variable referring to function's this
- * @type: Type of private data
- * @priv: Name for private data variable defined by this code snippet
- *
- * A convenience macro for getting the private data from GJS classes using
- * GIWrapper.
- * Throws an error and returns false if the 'this' object is not the right type.
- * Use in any JSNative function.
- */
-#define GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, thisobj, type, priv) \
-    GJS_GET_THIS(cx, argc, vp, args, thisobj);                        \
-    type* priv = type::for_js_typecheck(cx, thisobj, args);           \
-    if (!priv)                                                        \
-        return false;
-
 /*
  * GIWrapperBase:
  *
@@ -130,7 +106,7 @@ GJS_JSAPI_RETURN_CONVENTION bool gjs_define_static_methods(
  * https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
  */
 template <class Base, class Prototype, class Instance>
-class GIWrapperBase {
+class GIWrapperBase : public CWrapperPointerOps<Base> {
  protected:
     // nullptr if this Base is a Prototype; points to the corresponding
     // Prototype if this Base is an Instance.
@@ -144,65 +120,7 @@ class GIWrapperBase {
     static constexpr JSFunctionSpec* proto_methods = nullptr;
     static constexpr JSFunctionSpec* static_methods = nullptr;
 
-    // Methods to get an existing Base
-
  public:
-    /*
-     * GIWrapperBase::for_js:
-     *
-     * Gets the Base belonging to a particular JS object wrapper. Checks that
-     * the wrapper object has the right JSClass (Base::klass) and returns null
-     * if not. */
-    [[nodiscard]] static Base* for_js(JSContext* cx, JS::HandleObject wrapper) {
-        return static_cast<Base*>(
-            JS_GetInstancePrivate(cx, wrapper, &Base::klass, nullptr));
-    }
-
-    /*
-     * GIWrapperBase::check_jsclass:
-     *
-     * Checks if the given wrapper object has the right JSClass (Base::klass).
-     */
-    [[nodiscard]] static bool check_jsclass(JSContext* cx,
-                                            JS::HandleObject wrapper) {
-        return !!for_js(cx, wrapper);
-    }
-
-    /*
-     * GIWrapperBase::for_js_typecheck:
-     *
-     * Like for_js(), only throws a JS exception if the wrapper object has the
-     * wrong class. Use in JSNative functions, where you have access to a
-     * JS::CallArgs. The exception message will mention args.callee.
-     *
-     * The second overload can be used when you don't have access to an
-     * instance of JS::CallArgs. The exception message will be generic.
-     */
-    GJS_JSAPI_RETURN_CONVENTION
-    static Base* for_js_typecheck(
-        JSContext* cx, JS::HandleObject wrapper,
-        JS::CallArgs& args) {  // NOLINT(runtime/references)
-        return static_cast<Base*>(
-            JS_GetInstancePrivate(cx, wrapper, &Base::klass, &args));
-    }
-    GJS_JSAPI_RETURN_CONVENTION
-    static Base* for_js_typecheck(JSContext* cx, JS::HandleObject wrapper) {
-        if (!gjs_typecheck_instance(cx, wrapper, &Base::klass, true))
-            return nullptr;
-        return for_js(cx, wrapper);
-    }
-
-    /*
-     * GIWrapperBase::for_js_nocheck:
-     *
-     * Use when you don't have a JSContext* available. This method is infallible
-     * and cannot trigger a GC, so it's safe to use from finalize() and trace().
-     * (It can return null if no private data has been set yet on the wrapper.)
-     */
-    [[nodiscard]] static Base* for_js_nocheck(JSObject* wrapper) {
-        return static_cast<Base*>(JS_GetPrivate(wrapper));
-    }
-
     // Methods implementing our CRTP polymorphism scheme follow below. We don't
     // use standard C++ polymorphism because that would occupy another 8 bytes
     // for a vtable.
@@ -574,8 +492,9 @@ class GIWrapperBase {
     template <typename T = void>
     GJS_JSAPI_RETURN_CONVENTION static T* to_c_ptr(JSContext* cx,
                                                    JS::HandleObject obj) {
-        Base* priv = Base::for_js_typecheck(cx, obj);
-        if (!priv || !priv->check_is_instance(cx, "get a C pointer"))
+        Base* priv;
+        if (!Base::for_js_typecheck(cx, obj, &priv) ||
+            !priv->check_is_instance(cx, "get a C pointer"))
             return nullptr;
 
         return static_cast<T*>(priv->to_instance()->ptr());
@@ -640,9 +559,9 @@ class GIWrapperBase {
      * @expected_type: (nullable): GType to check
      *
      * Checks not only that the JS object is of the correct JSClass (like
-     * for_js_typecheck() does); but also that the object is an instance, not
-     * the protptype; and that the instance's wrapped pointer is of the correct
-     * GType or GI info.
+     * CWrapperPointerOps::typecheck() does); but also that the object is an
+     * instance, not the prototype; and that the instance's wrapped pointer is
+     * of the correct GType or GI info.
      *
      * The overload with a GjsTypecheckNoThrow parameter will not throw a JS
      * exception if the prototype is passed in or the typecheck fails.
@@ -650,8 +569,9 @@ class GIWrapperBase {
     GJS_JSAPI_RETURN_CONVENTION
     static bool typecheck(JSContext* cx, JS::HandleObject object,
                           GIBaseInfo* expected_info, GType expected_gtype) {
-        Base* priv = Base::for_js_typecheck(cx, object);
-        if (!priv || !priv->check_is_instance(cx, "convert to pointer"))
+        Base* priv;
+        if (!Base::for_js_typecheck(cx, object, &priv) ||
+            !priv->check_is_instance(cx, "convert to pointer"))
             return false;
 
         if (priv->to_instance()->typecheck_impl(cx, expected_info,
diff --git a/meson.build b/meson.build
index 38eba45c..b6735cb1 100644
--- a/meson.build
+++ b/meson.build
@@ -372,6 +372,7 @@ libgjs_sources = [
     'gi/arg-cache.cpp', 'gi/arg-cache.h',
     'gi/boxed.cpp', 'gi/boxed.h',
     'gi/closure.cpp', 'gi/closure.h',
+    'gi/cwrapper.cpp', 'gi/cwrapper.h',
     'gi/enumeration.cpp', 'gi/enumeration.h',
     'gi/foreign.cpp', 'gi/foreign.h',
     'gi/fundamental.cpp', 'gi/fundamental.h',


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