[gjs/ewlsh/fix-closures: 101/101] Support GClosure and invoke()
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/fix-closures: 101/101] Support GClosure and invoke()
- Date: Sat, 15 Jan 2022 01:21:17 +0000 (UTC)
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, >ype))
+ 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, >ype))
+ 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, >ype))
+ 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",
+ ¶ms, "return_type", &return_type))
+ return false;
+
+ GType gtype;
+
+ if (!gjs_gtype_get_actual_gtype(cx, closure, >ype))
+ 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]