[gjs/ewlsh/fix-closures: 101/101] Support GClosure and invoke()




commit ddfe43fd4bd782548d7679eac4a6b1373d28dc90
Author: Evan Welsh <contact evanwelsh com>
Date:   Fri Jan 14 17:21:00 2022 -0800

    Support GClosure and invoke()

 gi/arg-cache.cpp                         | 152 +++++++++++++++++++++++++++++-
 gi/arg-cache.h                           |   7 ++
 gi/arg.cpp                               |  42 ++++++---
 gi/arg.h                                 |   1 +
 gi/boxed.cpp                             |  10 +-
 gi/closure.h                             |  11 ++-
 gi/private.cpp                           | 156 +++++++++++++++++++++++++++++++
 gi/value.cpp                             |  29 ++++--
 installed-tests/js/meson.build           |   1 +
 installed-tests/js/testGIMarshalling.js  |  14 +--
 installed-tests/js/testGObject.js        |   2 +-
 installed-tests/js/testGObjectClosure.js | 138 +++++++++++++++++++++++++++
 modules/core/overrides/GObject.js        |  32 +++++++
 modules/core/overrides/Gtk.js            |  12 ++-
 14 files changed, 566 insertions(+), 41 deletions(-)
---
diff --git a/gi/arg-cache.cpp b/gi/arg-cache.cpp
index 2fda580c..60f19360 100644
--- a/gi/arg-cache.cpp
+++ b/gi/arg-cache.cpp
@@ -565,6 +565,12 @@ struct GValueIn : BoxedIn {
             JS::HandleValue) override;
 };
 
+struct GValueInOut : GValueIn, Positioned {
+    using GValueIn::GValueIn;
+    bool in(JSContext*, GjsFunctionCallState*, GIArgument*,
+            JS::HandleValue) override;
+};
+
 struct GValueInTransferNone : GValueIn {
     using GValueIn::GValueIn;
     bool release(JSContext*, GjsFunctionCallState*, GIArgument*,
@@ -766,6 +772,9 @@ bool GenericInOut::in(JSContext* cx, GjsFunctionCallState* state,
     if (!GenericIn::in(cx, state, arg, value))
         return false;
 
+    if (!(flags() & GjsArgumentFlags::CALLER_ALLOCATES))
+        state->ignore_release.insert(arg);
+
     state->out_cvalue(m_arg_pos) = state->inout_original_cvalue(m_arg_pos) =
         *arg;
     gjs_arg_set(arg, &state->out_cvalue(m_arg_pos));
@@ -1120,6 +1129,42 @@ bool GValueIn::in(JSContext* cx, GjsFunctionCallState* state, GIArgument* arg,
     return true;
 }
 
+bool GValueInOut::in(JSContext* cx, GjsFunctionCallState* state,
+                     GIArgument* arg, JS::HandleValue value) {
+    if (value.isNull()) {
+        if (Nullable::handle_nullable(cx, arg, m_arg_name)) {
+            state->out_cvalue(m_arg_pos) =
+                state->inout_original_cvalue(m_arg_pos) =
+                    state->in_cvalue(m_arg_pos) = *arg;
+            gjs_arg_set(arg, &state->out_cvalue(m_arg_pos));
+            return true;
+        }
+        return false;
+    }
+
+    if (value.isObject()) {
+        JS::RootedObject obj(cx, &value.toObject());
+        GType gtype;
+
+        if (!gjs_gtype_get_actual_gtype(cx, obj, &gtype))
+            return false;
+
+        if (gtype == G_TYPE_VALUE) {
+            gjs_arg_set(arg, BoxedBase::to_c_ptr<GValue>(cx, obj));
+
+            state->out_cvalue(m_arg_pos) =
+                state->inout_original_cvalue(m_arg_pos) = *arg;
+            state->ignore_release.insert(arg);
+            return true;
+        }
+    }
+
+    gjs_throw(cx,
+              "JavaScript values are not supported for inout GValues, use "
+              "GObject.Value instead.");
+    return false;
+}
+
 GJS_JSAPI_RETURN_CONVENTION
 bool BoxedInTransferNone::in(JSContext* cx, GjsFunctionCallState* state,
                              GIArgument* arg, JS::HandleValue value) {
@@ -1166,6 +1211,20 @@ bool GClosureInTransferNone::in(JSContext* cx, GjsFunctionCallState* state,
     if (value.isNull())
         return NullableIn::in(cx, state, arg, value);
 
+    if (value.isObject()) {
+        JS::RootedObject obj(cx, &value.toObject());
+        GType gtype;
+
+        if (!gjs_gtype_get_actual_gtype(cx, obj, &gtype))
+            return false;
+
+        if (gtype == G_TYPE_CLOSURE) {
+            gjs_arg_set(arg, BoxedBase::to_c_ptr<Gjs::Closure>(cx, obj));
+            state->ignore_release.insert(arg);
+            return true;
+        }
+    }
+
     if (!(JS_TypeOfValue(cx, value) == JSTYPE_FUNCTION))
         return report_typeof_mismatch(cx, m_arg_name, value,
                                       ExpectedType::FUNCTION);
@@ -1340,16 +1399,21 @@ bool GenericOut::release(JSContext* cx, GjsFunctionCallState*,
 
 GJS_JSAPI_RETURN_CONVENTION
 bool GenericInOut::release(JSContext* cx, GjsFunctionCallState* state,
-                           GIArgument*, GIArgument* out_arg) {
+                           GIArgument* in_arg, GIArgument* out_arg) {
     // For inout, transfer refers to what we get back from the function; for
     // the temporary C value we allocated, clearly we're responsible for
     // freeing it.
 
     GIArgument* original_out_arg = &state->inout_original_cvalue(m_arg_pos);
-    if (!gjs_g_argument_release_in_arg(cx, GI_TRANSFER_NOTHING, &m_type_info,
+
+    if (!state->ignore_release.erase(in_arg) &&
+        !gjs_g_argument_release_in_arg(cx, GI_TRANSFER_NOTHING, &m_type_info,
                                        original_out_arg))
         return false;
 
+    if (state->ignore_release.erase(out_arg))
+        return true;
+
     return gjs_g_argument_release(cx, m_transfer, &m_type_info, out_arg);
 }
 
@@ -2058,6 +2122,88 @@ void ArgsCache::build_normal_in_arg(uint8_t gi_index, GITypeInfo* type_info,
     }
 }
 
+void ArgsCache::build_interface_inout_arg(uint8_t gi_index,
+                                          GITypeInfo* type_info,
+                                          GIBaseInfo* interface_info,
+                                          GITransfer transfer, const char* name,
+                                          GjsArgumentFlags flags) {
+    GIInfoType interface_type = g_base_info_get_type(interface_info);
+
+    auto base_args =
+        std::make_tuple(gi_index, name, type_info, transfer, flags);
+    auto common_args =
+        std::tuple_cat(base_args, std::make_tuple(interface_info));
+
+    // We do some transfer magic later, so let's ensure we don't mess up.
+    // Should not happen in practice.
+    if (G_UNLIKELY(transfer == GI_TRANSFER_CONTAINER)) {
+        set_argument_auto<Arg::NotIntrospectable>(base_args,
+                                                  INTERFACE_TRANSFER_CONTAINER);
+        return;
+    }
+
+    switch (interface_type) {
+        case GI_INFO_TYPE_ENUM:
+        case GI_INFO_TYPE_FLAGS:
+        case GI_INFO_TYPE_STRUCT:
+        case GI_INFO_TYPE_BOXED:
+        case GI_INFO_TYPE_OBJECT:
+        case GI_INFO_TYPE_INTERFACE:
+        case GI_INFO_TYPE_UNION: {
+            GType gtype = g_registered_type_info_get_g_type(interface_info);
+
+            if (gtype == G_TYPE_VALUE) {
+                set_argument_auto<Arg::GValueInOut>(common_args);
+                return;
+            }
+
+            set_argument_auto<Arg::FallbackInOut>(base_args);
+            return;
+        } break;
+
+        case GI_INFO_TYPE_INVALID:
+        case GI_INFO_TYPE_FUNCTION:
+        case GI_INFO_TYPE_CALLBACK:
+        case GI_INFO_TYPE_CONSTANT:
+        case GI_INFO_TYPE_INVALID_0:
+        case GI_INFO_TYPE_VALUE:
+        case GI_INFO_TYPE_SIGNAL:
+        case GI_INFO_TYPE_VFUNC:
+        case GI_INFO_TYPE_PROPERTY:
+        case GI_INFO_TYPE_FIELD:
+        case GI_INFO_TYPE_ARG:
+        case GI_INFO_TYPE_TYPE:
+        case GI_INFO_TYPE_UNRESOLVED:
+        default:
+            // Don't know how to handle this interface type (should not happen
+            // in practice, for typelibs emitted by g-ir-compiler)
+            set_argument_auto<Arg::NotIntrospectable>(base_args,
+                                                      UNSUPPORTED_TYPE);
+    }
+}
+
+void ArgsCache::build_normal_inout_arg(uint8_t gi_index, GITypeInfo* type_info,
+                                       GIArgInfo* arg, GjsArgumentFlags flags) {
+    const char* name = g_base_info_get_name(arg);
+    GITransfer transfer = g_arg_info_get_ownership_transfer(arg);
+    auto common_args =
+        std::make_tuple(gi_index, name, type_info, transfer, flags);
+
+    GITypeTag tag = g_type_info_get_tag(type_info);
+
+    switch (tag) {
+        case GI_TYPE_TAG_INTERFACE: {
+            GjsAutoBaseInfo interface_info =
+                g_type_info_get_interface(type_info);
+            build_interface_inout_arg(gi_index, type_info, interface_info,
+                                      transfer, name, flags);
+            return;
+        }
+        default:
+            set_argument_auto<Arg::FallbackInOut>(common_args);
+    }
+}
+
 void ArgsCache::build_instance(GICallableInfo* callable) {
     if (!m_is_method)
         return;
@@ -2221,7 +2367,7 @@ void ArgsCache::build_arg(uint8_t gi_index, GIDirection direction,
     if (direction == GI_DIRECTION_IN)
         build_normal_in_arg(gi_index, &type_info, arg, flags);
     else if (direction == GI_DIRECTION_INOUT)
-        set_argument_auto<Arg::FallbackInOut>(common_args);
+        build_normal_inout_arg(gi_index, &type_info, arg, flags);
     else
         set_argument_auto<Arg::FallbackOut>(common_args);
 
diff --git a/gi/arg-cache.h b/gi/arg-cache.h
index 2a02869b..0dc387c1 100644
--- a/gi/arg-cache.h
+++ b/gi/arg-cache.h
@@ -173,6 +173,13 @@ struct ArgsCache {
     void build_interface_in_arg(uint8_t gi_index, GITypeInfo*, GIBaseInfo*,
                                 GITransfer, const char* name, GjsArgumentFlags);
 
+    void build_normal_inout_arg(uint8_t gi_index, GITypeInfo*, GIArgInfo*,
+                                GjsArgumentFlags);
+
+    void build_interface_inout_arg(uint8_t gi_index, GITypeInfo*, GIBaseInfo*,
+                                   GITransfer, const char* name,
+                                   GjsArgumentFlags);
+
     template <typename T, Arg::Kind ArgKind = Arg::Kind::NORMAL,
               typename... Args>
     T* set_argument(uint8_t index, const char* name, GITypeInfo*, GITransfer,
diff --git a/gi/arg.cpp b/gi/arg.cpp
index 99446bf7..01c3781e 100644
--- a/gi/arg.cpp
+++ b/gi/arg.cpp
@@ -1225,6 +1225,21 @@ static bool value_to_interface_gi_argument(
             return true;
         }
 
+        if (value.isObject()) {
+            JS::RootedObject obj(cx, &value.toObject());
+            GType gtype;
+
+            if (!gjs_gtype_get_actual_gtype(cx, obj, &gtype))
+                return false;
+
+            if (gtype == G_TYPE_VALUE) {
+                GValue* value = BoxedBase::to_c_ptr<GValue>(cx, obj);
+                gjs_arg_set(arg, value);
+
+                return true;
+            }
+        }
+
         Gjs::AutoGValue gvalue;
         if (!gjs_value_to_g_value(cx, value, &gvalue)) {
             gjs_arg_unset<void*>(arg);
@@ -1346,17 +1361,23 @@ static bool value_to_interface_gi_argument(
 
             } else if (g_type_is_a(gtype, G_TYPE_BOXED)) {
                 if (g_type_is_a(gtype, G_TYPE_CLOSURE)) {
-                    GClosure* closure = Gjs::Closure::create_marshaled(
-                        cx, JS_GetObjectFunction(obj), "boxed");
-                    // GI doesn't know about floating GClosure references. We
-                    // guess that if this is a return value going from JS::Value
-                    // to GArgument, it's intended to be passed to a C API that
-                    // will consume the floating reference.
-                    if (arg_type != GJS_ARGUMENT_RETURN_VALUE) {
-                        g_closure_ref(closure);
-                        g_closure_sink(closure);
+                    if (JS_ObjectIsFunction(obj)) {
+                        GClosure* closure = Gjs::Closure::create_marshaled(
+                            cx, JS_GetObjectFunction(obj), "boxed");
+                        // GI doesn't know about floating GClosure references.
+                        // We guess that if this is a return value going from
+                        // JS::Value to GArgument, it's intended to be passed to
+                        // a C API that will consume the floating reference.
+                        if (arg_type != GJS_ARGUMENT_RETURN_VALUE) {
+                            g_closure_ref(closure);
+                            g_closure_sink(closure);
+                        }
+
+                        gjs_arg_set(arg, closure);
+                        return true;
                     }
-                    gjs_arg_set(arg, closure);
+
+                    gjs_arg_set(arg, BoxedBase::to_c_ptr<GClosure>(cx, obj));
                     return true;
                 }
 
@@ -1557,7 +1578,6 @@ bool gjs_value_to_g_argument(JSContext* context, JS::HandleValue value,
             return false;
         }
         break;
-
     case GI_TYPE_TAG_GTYPE:
         if (value.isObjectOrNull()) {
             GType gtype;
diff --git a/gi/arg.h b/gi/arg.h
index c1ac6955..17792a30 100644
--- a/gi/arg.h
+++ b/gi/arg.h
@@ -39,6 +39,7 @@ enum class GjsArgumentFlags : uint8_t {
     ARG_IN = 1 << 4,
     ARG_OUT = 1 << 5,
     ARG_INOUT = ARG_IN | ARG_OUT,
+    OPTIONAL = 1 << 5,
 };
 
 [[nodiscard]] char* gjs_argument_display_name(const char* arg_name,
diff --git a/gi/boxed.cpp b/gi/boxed.cpp
index 92af1366..f5e9efde 100644
--- a/gi/boxed.cpp
+++ b/gi/boxed.cpp
@@ -323,18 +323,18 @@ bool BoxedInstance::constructor_impl(JSContext* context, JS::HandleObject obj,
         }
     }
 
-    if (gtype() == G_TYPE_VARIANT) {
+    if (gtype() == G_TYPE_VARIANT || gtype() == G_TYPE_CLOSURE) {
         /* Short-circuit construction for GVariants by calling into the JS packing
            function */
         const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
         if (!boxed_invoke_constructor(context, obj, atoms.new_internal(), args))
             return false;
 
-        // The return value of GLib.Variant.new_internal() gets its own
-        // BoxedInstance, and the one we're setting up in this constructor is
-        // discarded.
+        // The return values of GLib.Variant.new_internal() and
+        // GObject.Closure.new_internal() gets their own BoxedInstance,
+        // and the one we're setting up in this constructor is discarded.
         debug_lifecycle(
-            "Boxed construction delegated to GVariant constructor, "
+            "Boxed construction delegated to JavaScript constructor, "
             "boxed object discarded");
 
         return true;
diff --git a/gi/closure.h b/gi/closure.h
index a9110fc5..23007402 100644
--- a/gi/closure.h
+++ b/gi/closure.h
@@ -27,6 +27,10 @@ class HandleValueArray;
 
 namespace Gjs {
 
+struct SignalClosureMeta {
+    uint32_t signal_id;
+};
+
 class Closure : public GClosure {
  protected:
     Closure(JSContext*, JSFunction*, bool root, const char* description);
@@ -79,11 +83,12 @@ class Closure : public GClosure {
     [[nodiscard]] static Closure* create_for_signal(JSContext* cx,
                                                     JSFunction* callable,
                                                     const char* description,
-                                                    int signal_id) {
+                                                    int32_t signal_id) {
         auto* self = new Closure(cx, callable, false /* root */, description);
         self->add_finalize_notifier<Closure>();
-        g_closure_set_meta_marshal(self, gjs_int_to_pointer(signal_id),
-                                   marshal_cb);
+        SignalClosureMeta* meta = new SignalClosureMeta();
+        meta->signal_id = signal_id;
+        g_closure_set_meta_marshal(self, meta, marshal_cb);
         return self;
     }
 
diff --git a/gi/private.cpp b/gi/private.cpp
index 4a07afb1..1bce1b8c 100644
--- a/gi/private.cpp
+++ b/gi/private.cpp
@@ -18,6 +18,7 @@
 #include <js/Utility.h>  // for UniqueChars
 #include <jsapi.h>       // for JS_GetElement
 
+#include "gi/boxed.h"
 #include "gi/gobject.h"
 #include "gi/gtype.h"
 #include "gi/interface.h"
@@ -238,6 +239,157 @@ static inline void gjs_add_interface(GType instance_type,
                                 &interface_vtable);
 }
 
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_value_from_closure(JSContext* cx, Gjs::Closure* closure,
+                                   JS::MutableHandleValue value) {
+    GjsAutoStructInfo info =
+        g_irepository_find_by_gtype(nullptr, G_TYPE_CLOSURE);
+    g_assert(info);
+
+    JS::RootedObject boxed(cx, BoxedInstance::new_for_c_struct(
+                                   cx, info, closure, BoxedInstance::NoCopy()));
+    if (!boxed)
+        return false;
+
+    value.setObject(*boxed);
+    return true;
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_create_closure(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    JS::RootedObject callable(cx);
+
+    if (!gjs_parse_call_args(cx, "create_closure", args, "o", "callable",
+                             &callable))
+        return false;
+
+    if (!JS_ObjectIsFunction(callable)) {
+        gjs_throw(cx, "create_closure() expects a callable function");
+        return false;
+    }
+
+    JS::RootedFunction func(cx, JS_GetObjectFunction(callable));
+
+    Gjs::Closure* closure =
+        Gjs::Closure::create_marshaled(cx, func, "custom callback");
+    if (closure == nullptr)
+        return false;
+
+    return gjs_value_from_closure(cx, closure, args.rval());
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_create_signal_closure(JSContext* cx, unsigned argc,
+                                      JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    JS::RootedObject owner(cx), callable(cx);
+    uint32_t signal_id = 0;
+
+    if (!gjs_parse_call_args(cx, "create_signal_closure", args, "oou", "owner",
+                             &owner, "callable", &callable, "signal_id",
+                             &signal_id))
+        return false;
+
+    if (!JS_ObjectIsFunction(callable)) {
+        gjs_throw(cx, "create_signal_closure() expects a callable function");
+        return false;
+    }
+
+    ObjectBase* base;
+    if (!ObjectInstance::for_js_typecheck(cx, owner, &base))
+        return false;
+
+    if (!base->check_is_instance(cx, "signal hookup"))
+        return false;
+
+    ObjectInstance* instance = base->to_instance();
+
+    JS::RootedFunction func(cx, JS_GetObjectFunction(callable));
+
+    Gjs::Closure* closure = Gjs::Closure::create_for_signal(
+        cx, func, "custom signal callback", signal_id);
+
+    if (closure == nullptr)
+        return false;
+    if (!instance->associate_closure(cx, closure))
+        return false;
+
+    return gjs_value_from_closure(cx, closure, args.rval());
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_invoke_closure(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    JS::RootedObject closure(cx);
+    JS::RootedObject this_object(cx);
+    JS::RootedObject params(cx);
+    JS::RootedObject return_type(cx);
+
+    if (!gjs_parse_call_args(cx, "invoke_closure", args, "o?oo?o", "closure",
+                             &closure, "this_object", &this_object, "params",
+                             &params, "return_type", &return_type))
+        return false;
+
+    GType gtype;
+
+    if (!gjs_gtype_get_actual_gtype(cx, closure, &gtype))
+        return false;
+
+    if (gtype != G_TYPE_CLOSURE) {
+        gjs_throw(cx, "Expected closure.");
+        return false;
+    }
+
+    Gjs::Closure* gjs_closure = BoxedBase::to_c_ptr<Gjs::Closure>(cx, closure);
+    if (closure == nullptr)
+        return false;
+
+    bool isArray;
+    if (!JS::IsArrayObject(cx, params, &isArray))
+        return false;
+    if (!isArray) {
+        gjs_throw(cx, "No array.");
+        return false;
+    }
+
+    uint32_t length;
+    if (!JS::GetArrayLength(cx, params, &length))
+        return false;
+    JS::RootedValue elem(cx);
+    AutoGValueVector param_values;
+    param_values.reserve(length);
+    for (uint32_t i = 0; i < length; i++) {
+        if (!JS_GetElement(cx, params, i, &elem))
+            return false;
+        Gjs::AutoGValue& value = param_values.emplace_back();
+        if (!gjs_value_to_g_value(cx, elem, &value))
+            return false;
+    }
+
+    if (return_type) {
+        GValue return_value = {0};
+        GType return_gtype;
+        if (!gjs_gtype_get_actual_gtype(cx, return_type, &return_gtype))
+            return false;
+        if (return_gtype == G_TYPE_INVALID) {
+            gjs_throw(cx, "Invalid gtype.");
+            return false;
+        }
+        g_value_init(&return_value, return_gtype);
+        g_closure_invoke(gjs_closure, &return_value, param_values.size(),
+                         param_values.data(), nullptr);
+        return gjs_value_from_g_value(cx, args.rval(), &return_value);
+    }
+    g_closure_invoke(gjs_closure, nullptr, param_values.size(),
+                     param_values.data(), nullptr);
+    args.rval().setNull();
+    return true;
+}
+
 GJS_JSAPI_RETURN_CONVENTION
 static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
     JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
@@ -409,6 +561,10 @@ static JSFunctionSpec private_module_funcs[] = {
           GJS_MODULE_PROP_FLAGS),
     JS_FN("register_type", gjs_register_type, 4, GJS_MODULE_PROP_FLAGS),
     JS_FN("signal_new", gjs_signal_new, 6, GJS_MODULE_PROP_FLAGS),
+    JS_FN("create_closure", gjs_create_closure, 1, GJS_MODULE_PROP_FLAGS),
+    JS_FN("create_signal_closure", gjs_create_signal_closure, 3,
+          GJS_MODULE_PROP_FLAGS),
+    JS_FN("invoke_closure", gjs_invoke_closure, 3, GJS_MODULE_PROP_FLAGS),
     JS_FS_END,
 };
 
diff --git a/gi/value.cpp b/gi/value.cpp
index cc85b654..a28d3ff2 100644
--- a/gi/value.cpp
+++ b/gi/value.cpp
@@ -160,9 +160,23 @@ void Gjs::Closure::marshal(GValue* return_value, unsigned n_param_values,
 
     if (marshal_data) {
         /* we are used for a signal handler */
-        guint signal_id;
+        SignalClosureMeta* signal_meta =
+            static_cast<SignalClosureMeta*>(marshal_data);
+        uint32_t signal_id = signal_meta->signal_id;
+
+        if (signal_id == 0) {
+            GSignalInvocationHint* hint =
+                static_cast<GSignalInvocationHint*>(invocation_hint);
+
+            if (!hint) {
+                gjs_debug(GJS_DEBUG_GCLOSURE,
+                          "Closure is not a signal handler but is being "
+                          "handled like one.");
+                return;
+            }
 
-        signal_id = GPOINTER_TO_UINT(marshal_data);
+            signal_id = hint->signal_id;
+        }
 
         g_signal_query(signal_id, &signal_query);
 
@@ -764,11 +778,12 @@ gjs_value_to_g_value_internal(JSContext      *context,
         if (!FundamentalBase::to_gvalue(context, fundamental_object, gvalue))
             return false;
     } else {
-        gjs_debug(GJS_DEBUG_GCLOSURE, "JS::Value is number %d gtype fundamental %d transformable to int %d 
from int %d",
-                  value.isNumber(),
-                  G_TYPE_IS_FUNDAMENTAL(gtype),
-                  g_value_type_transformable(gtype, G_TYPE_INT),
-                  g_value_type_transformable(G_TYPE_INT, gtype));
+        gjs_debug(GJS_DEBUG_GCLOSURE,
+                  "JS::Value is number %d\ngtype fundamental %d\ntransformable "
+                  "to int %s\ntransformable from int %s",
+                  value.isNumber(), G_TYPE_IS_FUNDAMENTAL(gtype),
+                  g_value_type_transformable(gtype, G_TYPE_INT) ? "yes" : "no",
+                  g_value_type_transformable(G_TYPE_INT, gtype) ? "yes" : "no");
 
         gjs_throw(context,
                   "Don't know how to convert JavaScript object to GType %s",
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index 2f007351..06343a5f 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -123,6 +123,7 @@ jasmine_tests = [
     'GLib',
     'GObject',
     'GObjectClass',
+    'GObjectClosure',
     'GObjectInterface',
     'GObjectValue',
     'GTypeClass',
diff --git a/installed-tests/js/testGIMarshalling.js b/installed-tests/js/testGIMarshalling.js
index 7d524919..06ee19e5 100644
--- a/installed-tests/js/testGIMarshalling.js
+++ b/installed-tests/js/testGIMarshalling.js
@@ -79,9 +79,6 @@ function testTransferMarshalling(root, value, inoutValue, options = {}) {
             in: {
                 omit: true,  // this case is not in the test suite
             },
-            inout: {
-                skip: 'https://gitlab.gnome.org/GNOME/gobject-introspection/issues/192',
-            },
         };
         Object.assign(fullOptions, options.full);
         testSimpleMarshalling(`${root}_full`, value, inoutValue, fullOptions);
@@ -896,12 +893,11 @@ describe('Callback', function () {
     describe('GClosure', function () {
         testInParameter('gclosure', () => 42);
 
-        xit('marshals a GClosure as a return value', function () {
-            // Currently a GObject.Closure instance is returned, upon which it's
-            // not possible to call invoke() because that method takes a bare
-            // pointer as an argument.
-            expect(GIMarshallingTests.gclosure_return()()).toEqual(42);
-        }).pend('https://gitlab.gnome.org/GNOME/gjs/issues/80');
+        it('marshals a GClosure as a return value', function () {
+            const closure = GIMarshallingTests.gclosure_return();
+            expect(closure instanceof GObject.Closure).toBeTruthy();
+            expect(closure.invoke(GObject.TYPE_INT, [])).toEqual(42);
+        });
     });
 
     it('marshals a return value', function () {
diff --git a/installed-tests/js/testGObject.js b/installed-tests/js/testGObject.js
index d6aafbbc..b41e6a26 100644
--- a/installed-tests/js/testGObject.js
+++ b/installed-tests/js/testGObject.js
@@ -62,7 +62,7 @@ describe('GObject overrides', function () {
 });
 
 describe('GObject should', function () {
-    const types = ['gpointer', 'GBoxed', 'GParam', 'GInterface', 'GObject', 'GVariant'];
+    const types = ['gpointer', 'GBoxed', 'GParam', 'GInterface', 'GObject', 'GVariant', 'GClosure'];
 
     types.forEach(type => {
         it(`be able to create a GType object for ${type}`, function () {
diff --git a/installed-tests/js/testGObjectClosure.js b/installed-tests/js/testGObjectClosure.js
new file mode 100644
index 00000000..87cbf6bb
--- /dev/null
+++ b/installed-tests/js/testGObjectClosure.js
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2020 Marco Trevisan <marco trevisan canonical com>
+
+const {GLib, GObject, GIMarshallingTests, Regress} = imports.gi;
+
+const SIGNED_TYPES = ['schar', 'int', 'int64', 'long'];
+const UNSIGNED_TYPES = ['char', 'uchar', 'uint', 'uint64', 'ulong'];
+const FLOATING_TYPES = ['double', 'float'];
+const NUMERIC_TYPES = [...SIGNED_TYPES, ...UNSIGNED_TYPES, ...FLOATING_TYPES];
+const SPECIFIC_TYPES = ['gtype', 'boolean', 'string', 'param', 'variant', 'boxed', 'gvalue'];
+const INSTANCED_TYPES = ['object', 'instance'];
+const ALL_TYPES = [...NUMERIC_TYPES, ...SPECIFIC_TYPES, ...INSTANCED_TYPES];
+
+describe('GObject closure (GClosure)', function () {
+    let spyFn;
+    let closure;
+    beforeEach(function () {
+        spyFn = jasmine.createSpy();
+        closure = new GObject.Closure(spyFn);
+    });
+
+    function getDefaultContentByType(type) {
+        if (SIGNED_TYPES.includes(type))
+            return -((Math.random() * 100 | 0) + 1);
+        if (UNSIGNED_TYPES.includes(type))
+            return -getDefaultContentByType('int') + 2;
+        if (FLOATING_TYPES.includes(type))
+            return getDefaultContentByType('uint') + 0.5;
+        if (type === 'string')
+            return `Hello GValue! ${getDefaultContentByType('uint')}`;
+        if (type === 'boolean')
+            return !!(getDefaultContentByType('int') % 2);
+        if (type === 'gtype')
+            return getGType(ALL_TYPES[Math.random() * ALL_TYPES.length | 0]);
+
+        if (type === 'boxed' || type === 'boxed-struct') {
+            return new GIMarshallingTests.BoxedStruct({
+                long_: getDefaultContentByType('long'),
+                // string_: getDefaultContentByType('string'), not supported
+            });
+        }
+        if (type === 'object') {
+            const wasCreatingObject = this.creatingObject;
+            this.creatingObject = true;
+            const props = ALL_TYPES.filter(e =>
+                (e !== 'object' || !wasCreatingObject) &&
+                e !== 'boxed' &&
+                e !== 'gtype' &&
+                e !== 'instance' &&
+                e !== 'param' &&
+                // Include string when gobject-introspection!268 is merged
+                e !== 'string' &&
+                e !== 'schar').concat([
+                'boxed-struct',
+            ]).reduce((ac, a) => ({
+                ...ac, [`some-${a}`]: getDefaultContentByType(a),
+            }), {});
+            delete this.creatingObject;
+            return new GIMarshallingTests.PropertiesObject(props);
+        }
+        if (type === 'param') {
+            return GObject.ParamSpec.string('test-param', '', getDefaultContentByType('string'),
+                GObject.ParamFlags.READABLE, '');
+        }
+        if (type === 'variant') {
+            return new GLib.Variant('a{sv}', {
+                pasta: new GLib.Variant('s', 'Carbonara (con guanciale)'),
+                pizza: new GLib.Variant('s', 'Verace'),
+                randomString: new GLib.Variant('s', getDefaultContentByType('string')),
+            });
+        }
+        if (type === 'gvalue') {
+            const value = new GObject.Value();
+            const valueType = NUMERIC_TYPES[Math.random() * NUMERIC_TYPES.length | 0];
+            value.init(getGType(valueType));
+            return value;
+        }
+        if (type === 'instance')
+            return new Regress.TestFundamentalSubObject(getDefaultContentByType('string'));
+
+
+        throw new Error(`No default content set for type ${type}`);
+    }
+
+    function getGType(type) {
+        if (type === 'schar')
+            return GObject.TYPE_CHAR;
+
+        if (type === 'boxed' || type === 'gvalue' || type === 'instance')
+            return getDefaultContentByType(type).constructor.$gtype;
+
+        return GObject[`TYPE_${type.toUpperCase()}`];
+    }
+
+    function skipUnsupported(type) {
+        if (type === 'boxed')
+            pending('https://gitlab.gnome.org/GNOME/gjs/-/issues/402');
+
+        if (type === 'gvalue')
+            pending('https://gitlab.gnome.org/GNOME/gjs/-/issues/272');
+    }
+
+    it('is an instanceof GObject.Closure', function () {
+        expect(closure instanceof GObject.Closure).toBeTruthy();
+    });
+
+    ALL_TYPES.forEach(type => {
+        const gtype = getGType(type);
+
+        it(`can return ${type}`, function () {
+            let randomContent = getDefaultContentByType(type);
+
+            skipUnsupported(type);
+            spyFn.and.returnValue(randomContent);
+            expect(closure.invoke(gtype, [])).toEqual(randomContent);
+        });
+    });
+
+    it('can be invalidated', function () {
+        spyFn.and.returnValue(13);
+        expect(closure.invoke(GObject.TYPE_INT, [])).toBe(13);
+        closure.invalidate();
+        expect(closure.invoke(null, [])).toBe(null);
+    });
+
+    it('can be called with parameters', function () {
+        const plusClosure = new GObject.Closure((a, b) => {
+            return a + b;
+        });
+
+        expect(plusClosure.invoke(GObject.TYPE_INT, [5, 6])).toBe(11);
+        expect(plusClosure.invoke(GObject.TYPE_STRING, ['hello', ', world'])).toBe('hello, world');
+    });
+
+    afterEach(function () {
+        closure = null;
+    });
+});
diff --git a/modules/core/overrides/GObject.js b/modules/core/overrides/GObject.js
index 6bfaf144..d672921b 100644
--- a/modules/core/overrides/GObject.js
+++ b/modules/core/overrides/GObject.js
@@ -753,4 +753,36 @@ function _init() {
         throw new Error('GObject.signal_handlers_disconnect_by_data() is not \
 introspectable. Use GObject.signal_handlers_disconnect_by_func() instead.');
     };
+
+    GObject.Closure._new_internal = function (callable) {
+        'sensitive';
+
+        return Gi.create_closure(callable);
+    };
+
+    GObject.Closure.new_simple = function () {
+        throw new Error('GObject.Closure.new_simple() is not introspectable. \
+Use new GObject.Closure() instead.');
+    };
+
+    GObject.Closure.new_object = function () {
+        throw new Error('GObject.Closure.new_object() is not introspectable. \
+Use new GObject.Closure() instead.');
+    };
+
+    const invoke_closure = Gi.invoke_closure;
+
+    /**
+     * @param {GType | null} [return_type] the GType of the return value or null if the closure returns void
+     * @param {any[]} [parameters] a list of values to pass to the closure
+     * @returns {any}
+     */
+    GObject.Closure.prototype.invoke = function (return_type = null, parameters = []) {
+        'hide source';
+
+        if (return_type === null)
+            return invoke_closure(this, null, parameters, return_type);
+
+        return invoke_closure(this, null, parameters, return_type);
+    };
 }
diff --git a/modules/core/overrides/Gtk.js b/modules/core/overrides/Gtk.js
index 77649a73..d974bed5 100644
--- a/modules/core/overrides/Gtk.js
+++ b/modules/core/overrides/Gtk.js
@@ -3,6 +3,7 @@
 // SPDX-FileCopyrightText: 2013 Giovanni Campagna
 
 const Legacy = imports._legacy;
+const Gi = imports._gi;
 const {Gio, GjsPrivate, GObject} = imports.gi;
 
 let Gtk;
@@ -124,14 +125,21 @@ function _init() {
         }, class extends GObject.Object {
             vfunc_create_closure(builder, handlerName, flags, connectObject) {
                 const swapped = flags & Gtk.BuilderClosureFlags.SWAPPED;
-                return _createClosure(
+                return _wrapInSignalMeta(connectObject, _createClosure(
                     builder, builder.get_current_object(),
-                    handlerName, swapped, connectObject);
+                    handlerName, swapped, connectObject));
             }
         });
     }
 }
 
+function _wrapInSignalMeta(connectObject, callable) {
+    if (connectObject instanceof GObject.Object)
+        return Gi.create_signal_closure(connectObject, callable, 0);
+    else
+        return callable;
+}
+
 function _createClosure(builder, thisArg, handlerName, swapped, connectObject) {
     connectObject = connectObject || thisArg;
 


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