diff --git a/gi/arg.cpp b/gi/arg.cpp index 49224e8394d8db369bc147f72de22df059ee6237..f5f83202f38a5014171f24e811696d702f880d4b 100644 --- a/gi/arg.cpp +++ b/gi/arg.cpp @@ -2681,13 +2681,10 @@ gjs_value_from_g_argument (JSContext *context, } if (g_type_is_a(gtype, G_TYPE_OBJECT)) { - // Null arg is already handled above - JSObject* obj = ObjectInstance::wrapper_from_gobject( - context, G_OBJECT(gjs_arg_get(arg))); - if (!obj) - return false; - value_p.setObject(*obj); - return true; + g_assert(gjs_arg_get(arg) && + "Null arg is already handled above"); + return ObjectInstance::set_value_from_gobject( + context, gjs_arg_get(arg), value_p); } if (g_type_is_a(gtype, G_TYPE_BOXED) || diff --git a/gi/function.cpp b/gi/function.cpp index b0a27a7c5cb1f710170c0e930b7d33c3b3ea0625..3c3cacbcb781d6120a003230b766d92062bac07d 100644 --- a/gi/function.cpp +++ b/gi/function.cpp @@ -365,6 +365,11 @@ void GjsCallbackTrampoline::callback_closure(GIArgument** args, void* result) { if (gobj) { this_object = ObjectInstance::wrapper_from_gobject(context, gobj); if (!this_object) { + if (g_object_get_qdata(gobj, ObjectBase::disposed_quark())) { + warn_about_illegal_js_callback( + "on disposed object", + "using the destroy(), dispose(), or remove() vfuncs"); + } gjs_log_exception(context); return; } diff --git a/gi/object.cpp b/gi/object.cpp index 39ce7f5fd5a84072b8fd020e57fef7b615eb1346..e253b8430562db51cdf9f95624d8a668ced41caa 100644 --- a/gi/object.cpp +++ b/gi/object.cpp @@ -10,6 +10,7 @@ #include // for find #include // for mem_fn +#include #include #include // for tie #include // for move @@ -46,6 +47,7 @@ #include "gi/object.h" #include "gi/repo.h" #include "gi/toggle.h" +#include "gi/utils-inl.h" // for gjs_int_to_pointer #include "gi/value.h" #include "gi/wrapperutils.h" #include "gjs/atoms.h" @@ -79,9 +81,12 @@ static_assert(sizeof(ObjectInstance) <= 88, bool ObjectInstance::s_weak_pointer_callback = false; ObjectInstance *ObjectInstance::wrapped_gobject_list = nullptr; +static const auto DISPOSED_OBJECT = std::numeric_limits::max(); + // clang-format off G_DEFINE_QUARK(gjs::custom-type, ObjectBase::custom_type) G_DEFINE_QUARK(gjs::custom-property, ObjectBase::custom_property) +G_DEFINE_QUARK(gjs::disposed, ObjectBase::disposed) // clang-format on [[nodiscard]] static GQuark gjs_object_priv_quark() { @@ -237,13 +242,29 @@ void ObjectPrototype::set_type_qdata(void) { void ObjectInstance::set_object_qdata(void) { - g_object_set_qdata(m_ptr, gjs_object_priv_quark(), this); + g_object_set_qdata_full( + m_ptr, gjs_object_priv_quark(), this, [](void* object) { + auto* self = static_cast(object); + if (G_UNLIKELY(!self->m_gobj_disposed)) { + g_warning( + "Object %p (a %s) was finalized but we didn't track " + "its disposal", + self->m_ptr.get(), g_type_name(self->gtype())); + self->m_gobj_disposed = true; + } + self->m_gobj_finalized = true; + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, + "Wrapped GObject %p finalized", + self->m_ptr.get()); + }); } void ObjectInstance::unset_object_qdata(void) { - g_object_set_qdata(m_ptr, gjs_object_priv_quark(), nullptr); + auto priv_quark = gjs_object_priv_quark(); + if (g_object_get_qdata(m_ptr, priv_quark) == this) + g_object_steal_qdata(m_ptr, priv_quark); } GParamSpec* ObjectPrototype::find_param_spec_from_id(JSContext* cx, @@ -299,10 +320,11 @@ bool ObjectInstance::add_property_impl(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::HandleValue) { debug_jsprop("Add property hook", id, obj); - if (is_custom_js_class() || m_gobj_disposed) + if (is_custom_js_class()) return true; ensure_uses_toggle_ref(cx); + return true; } @@ -1084,18 +1106,43 @@ static void wrapped_gobj_dispose_notify( where_the_object_was); } -static void wrapped_gobj_toggle_notify(void*, GObject* gobj, - gboolean is_last_ref); +void ObjectInstance::track_gobject_finalization() { + auto quark = ObjectBase::disposed_quark(); + g_object_steal_qdata(m_ptr, quark); + g_object_set_qdata_full(m_ptr, quark, this, [](void* data) { + auto* self = static_cast(data); + self->m_gobj_finalized = true; + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Wrapped GObject %p finalized", + self->m_ptr.get()); + }); +} + +void ObjectInstance::ignore_gobject_finalization() { + auto quark = ObjectBase::disposed_quark(); + if (g_object_get_qdata(m_ptr, quark) == this) { + g_object_steal_qdata(m_ptr, quark); + g_object_set_qdata(m_ptr, quark, gjs_int_to_pointer(DISPOSED_OBJECT)); + } +} void ObjectInstance::gobj_dispose_notify(void) { m_gobj_disposed = true; + unset_object_qdata(); + track_gobject_finalization(); + if (m_uses_toggle_ref) { - g_object_remove_toggle_ref(m_ptr, wrapped_gobj_toggle_notify, nullptr); - wrapped_gobj_toggle_notify(nullptr, m_ptr, TRUE); + g_object_ref(m_ptr.get()); + g_object_remove_toggle_ref(m_ptr, wrapped_gobj_toggle_notify, this); + ToggleQueue::get_default().cancel(m_ptr); + wrapped_gobj_toggle_notify(this, m_ptr, TRUE); + m_uses_toggle_ref = false; } + + if (GjsContextPrivate::from_current_context()->is_owner_thread()) + discard_wrapper(); } void ObjectInstance::iterate_wrapped_gobjects( @@ -1210,22 +1257,38 @@ static void toggle_handler(GObject *gobj, ToggleQueue::Direction direction) { + auto* self = ObjectInstance::for_gobject(gobj); + + if (G_UNLIKELY(!self)) { + void* disposed = g_object_get_qdata(gobj, ObjectBase::disposed_quark()); + + if (G_UNLIKELY(disposed == gjs_int_to_pointer(DISPOSED_OBJECT))) { + g_critical("Handling toggle %s for an unknown object %p", + direction == ToggleQueue::UP ? "up" : "down", gobj); + return; + } + + // In this case the object has been disposed but its wrapper not yet + self = static_cast(disposed); + } + switch (direction) { case ToggleQueue::UP: - ObjectInstance::for_gobject(gobj)->toggle_up(); + self->toggle_up(); break; case ToggleQueue::DOWN: - ObjectInstance::for_gobject(gobj)->toggle_down(); + self->toggle_down(); break; default: g_assert_not_reached(); } } -static void wrapped_gobj_toggle_notify(void*, GObject* gobj, - gboolean is_last_ref) { +void ObjectInstance::wrapped_gobj_toggle_notify(void* instance, GObject* gobj, + gboolean is_last_ref) { bool is_main_thread; bool toggle_up_queued, toggle_down_queued; + auto* self = static_cast(instance); GjsContextPrivate* gjs = GjsContextPrivate::from_current_context(); if (gjs->destroying()) { @@ -1266,6 +1329,9 @@ static void wrapped_gobj_toggle_notify(void*, GObject* gobj, is_main_thread = gjs->is_owner_thread(); auto& toggle_queue = ToggleQueue::get_default(); + if (is_main_thread && toggle_queue.is_being_handled(gobj)) + return; + std::tie(toggle_down_queued, toggle_up_queued) = toggle_queue.is_queued(gobj); if (is_last_ref) { @@ -1273,16 +1339,16 @@ static void wrapped_gobj_toggle_notify(void*, GObject* gobj, * The JSObject is rooted and we need to unroot it so it * can be garbage collected */ - if (is_main_thread) { - if (G_UNLIKELY (toggle_up_queued || toggle_down_queued)) { - g_error("toggling down object %s that's already queued to toggle %s\n", - G_OBJECT_TYPE_NAME(gobj), - toggle_up_queued && toggle_down_queued? "up and down" : - toggle_up_queued? "up" : "down"); + if (is_main_thread && !toggle_up_queued) { + if (G_UNLIKELY(toggle_down_queued)) { + g_error( + "toggling down object %p (%s) that's already queued to " + "toggle down", + gobj, G_OBJECT_TYPE_NAME(gobj)); } - ObjectInstance::for_gobject(gobj)->toggle_down(); - } else { + self->toggle_down(); + } else if (!toggle_down_queued) { toggle_queue.enqueue(gobj, ToggleQueue::DOWN, toggle_handler); } } else { @@ -1293,11 +1359,13 @@ static void wrapped_gobj_toggle_notify(void*, GObject* gobj, */ if (is_main_thread && !toggle_down_queued) { if (G_UNLIKELY (toggle_up_queued)) { - g_error("toggling up object %s that's already queued to toggle up\n", - G_OBJECT_TYPE_NAME(gobj)); + g_error( + "toggling up object %p (%s) that's already queued to " + "toggle up", + gobj, G_OBJECT_TYPE_NAME(gobj)); } - ObjectInstance::for_gobject(gobj)->toggle_up(); - } else { + self->toggle_up(); + } else if (!toggle_up_queued) { toggle_queue.enqueue(gobj, ToggleQueue::UP, toggle_handler); } } @@ -1307,11 +1375,26 @@ void ObjectInstance::release_native_object(void) { discard_wrapper(); - if (m_uses_toggle_ref && m_gobj_disposed) + + if (m_gobj_finalized) { + g_critical( + "Object %p of type %s has been finalized while it was still " + "owned by gjs, this is due to invalid memory management.", + m_ptr.get(), g_type_name(gtype())); m_ptr.release(); - else if (m_uses_toggle_ref) + return; + } + + if (m_ptr) + gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Releasing native object %s %p", + g_type_name(gtype()), m_ptr.get()); + + if (m_gobj_disposed) + ignore_gobject_finalization(); + + if (m_uses_toggle_ref && !m_gobj_disposed) g_object_remove_toggle_ref(m_ptr.release(), wrapped_gobj_toggle_notify, - nullptr); + this); else m_ptr = nullptr; } @@ -1322,9 +1405,7 @@ ObjectInstance::release_native_object(void) void gjs_object_clear_toggles(void) { - auto& toggle_queue = ToggleQueue::get_default(); - while (toggle_queue.handle_toggle(toggle_handler)) - ; + ToggleQueue::get_default().handle_all_toggles(toggle_handler); } void @@ -1354,6 +1435,7 @@ ObjectInstance::ObjectInstance(JSContext* cx, JS::HandleObject object) : GIWrapperInstance(cx, object), m_wrapper_finalized(false), m_gobj_disposed(false), + m_gobj_finalized(false), m_uses_toggle_ref(false) { GTypeQuery query; type_query_dynamic_safe(&query); @@ -1392,7 +1474,22 @@ void ObjectInstance::update_heap_wrapper_weak_pointers(JSContext*, bool ObjectInstance::weak_pointer_was_finalized(void) { - if (has_wrapper() && !wrapper_is_rooted() && update_after_gc()) { + if (has_wrapper() && !wrapper_is_rooted()) { + bool toggle_down_queued, toggle_up_queued; + + auto& toggle_queue = ToggleQueue::get_default(); + std::tie(toggle_down_queued, toggle_up_queued) = + toggle_queue.is_queued(m_ptr); + + if (!toggle_down_queued && toggle_up_queued) + return false; + + if (!update_after_gc()) + return false; + + if (toggle_down_queued) + toggle_queue.cancel(m_ptr); + /* Ouch, the JS object is dead already. Disassociate the * GObject and hope the GObject dies too. (Remove it from * the weak pointer list first, since the disassociation @@ -1429,18 +1526,24 @@ ObjectInstance::associate_js_gobject(JSContext *context, m_ptr = gobj; set_object_qdata(); m_wrapper = object; + m_gobj_disposed = !!g_object_get_qdata(gobj, ObjectBase::disposed_quark()); ensure_weak_pointer_callback(context); link(); - g_object_weak_ref(gobj, wrapped_gobj_dispose_notify, this); + if (!G_UNLIKELY(m_gobj_disposed)) + g_object_weak_ref(gobj, wrapped_gobj_dispose_notify, this); } -void -ObjectInstance::ensure_uses_toggle_ref(JSContext *cx) -{ +// The return value here isn't intended to be JS API like boolean, as we only +// return whether the object has a toggle reference, and if we've added one +// and depending on this callers may need to unref the object on failure. +bool ObjectInstance::ensure_uses_toggle_ref(JSContext* cx) { if (m_uses_toggle_ref) - return; + return true; + + if (!check_gobject_disposed("add toggle reference on")) + return false; debug_lifecycle("Switching object instance to toggle ref"); @@ -1459,12 +1562,14 @@ ObjectInstance::ensure_uses_toggle_ref(JSContext *cx) */ m_uses_toggle_ref = true; switch_to_rooted(cx); - g_object_add_toggle_ref(m_ptr, wrapped_gobj_toggle_notify, nullptr); + g_object_add_toggle_ref(m_ptr, wrapped_gobj_toggle_notify, this); /* We now have both a ref and a toggle ref, we only want the toggle ref. * This may immediately remove the GC root we just added, since refcount * may drop to 1. */ g_object_unref(m_ptr); + + return true; } static void invalidate_closure_list(std::forward_list* closures) { @@ -1493,16 +1598,17 @@ ObjectInstance::disassociate_js_gobject(void) auto& toggle_queue = ToggleQueue::get_default(); std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(m_ptr.get()); - if (had_toggle_down != had_toggle_up) { + if (had_toggle_up && !had_toggle_down) { g_error( "JS object wrapper for GObject %p (%s) is being released while " "toggle references are still pending.", m_ptr.get(), type_name()); } - if (!m_gobj_disposed) { + if (!m_gobj_disposed) g_object_weak_unref(m_ptr.get(), wrapped_gobj_dispose_notify, this); + if (!m_gobj_finalized) { /* Fist, remove the wrapper pointer from the wrapped GObject */ unset_object_qdata(); } @@ -1577,10 +1683,11 @@ ObjectInstance::init_impl(JSContext *context, * we're not actually using it, so just let it get collected. Avoiding * this would require a non-trivial amount of work. * */ - other_priv->ensure_uses_toggle_ref(context); + if (!other_priv->ensure_uses_toggle_ref(context)) + gobj = nullptr; + object.set(other_priv->m_wrapper); - g_object_unref(gobj); /* We already own a reference */ - gobj = NULL; + g_clear_object(&gobj); /* We already own a reference */ return true; } @@ -1669,13 +1776,6 @@ ObjectInstance::~ObjectInstance() { bool had_toggle_up; bool had_toggle_down; - if (G_UNLIKELY(m_ptr->ref_count <= 0)) { - g_error( - "Finalizing wrapper for an already freed object of type: " - "%s.%s\n", - ns(), name()); - } - auto& toggle_queue = ToggleQueue::get_default(); std::tie(had_toggle_down, had_toggle_up) = toggle_queue.cancel(m_ptr); @@ -1688,6 +1788,10 @@ ObjectInstance::~ObjectInstance() { if (!m_gobj_disposed) g_object_weak_unref(m_ptr, wrapped_gobj_dispose_notify, this); + + if (!m_gobj_finalized) + unset_object_qdata(); + release_native_object(); } @@ -2237,11 +2341,13 @@ bool ObjectBase::to_string(JSContext* cx, unsigned argc, JS::Value* vp) { /* * ObjectInstance::to_string_kind: * - * ObjectInstance shows a "finalized" marker in its toString() method if the - * wrapped GObject has already been finalized. + * ObjectInstance shows a "disposed" marker in its toString() method if the + * wrapped GObject has already been disposed. */ const char* ObjectInstance::to_string_kind(void) const { - return m_gobj_disposed ? "object (FINALIZED)" : "object"; + if (m_gobj_finalized) + return "object (FINALIZED)"; + return m_gobj_disposed ? "object (DISPOSED)" : "object"; } /* @@ -2371,7 +2477,11 @@ bool ObjectInstance::init_custom_class_from_gobject(JSContext* cx, // Custom JS objects will most likely have visible state, so just do this // from the start. - ensure_uses_toggle_ref(cx); + if (!ensure_uses_toggle_ref(cx)) { + gjs_throw(cx, "Impossible to set toggle references on %sobject %p", + m_gobj_disposed ? "disposed " : "", gobj); + return false; + } const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); JS::RootedValue v(cx); @@ -2401,8 +2511,8 @@ ObjectInstance* ObjectInstance::new_for_gobject(JSContext* cx, GObject* gobj) { GType gtype = G_TYPE_FROM_INSTANCE(gobj); - gjs_debug_marshal(GJS_DEBUG_GOBJECT, "Wrapping %s with JSObject", - g_type_name(gtype)); + gjs_debug_marshal(GJS_DEBUG_GOBJECT, "Wrapping %s %p with JSObject", + g_type_name(gtype), gobj); JS::RootedObject proto(cx, gjs_lookup_object_prototype(cx, gtype)); if (!proto) @@ -2445,6 +2555,24 @@ JSObject* ObjectInstance::wrapper_from_gobject(JSContext* cx, GObject* gobj) { return priv->wrapper(); } +bool ObjectInstance::set_value_from_gobject(JSContext* cx, GObject* gobj, + JS::MutableHandleValue value_p) { + if (!gobj) { + value_p.setNull(); + return true; + } + + auto* wrapper = ObjectInstance::wrapper_from_gobject(cx, gobj); + if (wrapper) { + value_p.setObject(*wrapper); + return true; + } + + gjs_throw(cx, "Failed to find JS object for GObject %p of type %s", gobj, + g_type_name(G_TYPE_FROM_INSTANCE(gobj))); + return false; +} + // Replaces GIWrapperBase::to_c_ptr(). The GIWrapperBase version is deleted. bool ObjectBase::to_c_ptr(JSContext* cx, JS::HandleObject obj, GObject** ptr) { g_assert(ptr); diff --git a/gi/object.h b/gi/object.h index 3d7079713836a94a07a7a3410072f6e6d4471e5e..fd04caf6340640607cff0308bcf36cdff5401e9e 100644 --- a/gi/object.h +++ b/gi/object.h @@ -177,6 +177,7 @@ class ObjectBase [[nodiscard]] static GQuark custom_type_quark(); [[nodiscard]] static GQuark custom_property_quark(); + [[nodiscard]] static GQuark disposed_quark(); }; // See https://bugzilla.mozilla.org/show_bug.cgi?id=1614220 @@ -304,6 +305,7 @@ class ObjectInstance : public GIWrapperInstance #include "gi/toggle.h" +#include "gjs/jsapi-util.h" std::deque::iterator ToggleQueue::find_operation_locked(const GObject *gobj, @@ -43,12 +44,16 @@ ToggleQueue::find_and_erase_operation_locked(const GObject *gobj, return had_toggle; } +void ToggleQueue::handle_all_toggles(Handler handler) { + while (handle_toggle(handler)) + ; +} + gboolean ToggleQueue::idle_handle_toggle(void *data) { auto self = static_cast(data); - while (self->handle_toggle(self->m_toggle_handler)) - ; + self->handle_all_toggles(self->m_toggle_handler); return G_SOURCE_REMOVE; } @@ -85,9 +90,13 @@ std::pair ToggleQueue::cancel(GObject* gobj) { return {had_toggle_down, had_toggle_up}; } -bool -ToggleQueue::handle_toggle(Handler handler) -{ +bool ToggleQueue::is_being_handled(GObject* gobj) { + GObject* tmp_gobj = gobj; + + return m_toggling_gobj.compare_exchange_strong(tmp_gobj, gobj); +} + +bool ToggleQueue::handle_toggle(Handler handler) { Item item; { std::lock_guard hold(lock); @@ -95,13 +104,37 @@ ToggleQueue::handle_toggle(Handler handler) return false; item = q.front(); - handler(item.gobj, item.direction); q.pop_front(); } - debug("handle", item.gobj); - if (item.needs_unref) - g_object_unref(item.gobj); + /* When getting the object from the weak reference we're implicitly + * adding a new reference to the object, this may cause the toggle + * notification to be triggered again and this may lead to enqueuing + * the object again, so let's save the toggling object in an atomic + * pointer so that we can check it quickly to ensure that we're not + * recursing into ourself. + */ + GObject* null_gobj = nullptr; + m_toggling_gobj.compare_exchange_strong(null_gobj, item.gobj); + + if (item.direction == Direction::DOWN) { + GjsSmartPointer gobj( + static_cast(g_weak_ref_get(&item.weak_ref))); + + if (gobj) { + debug("handle", gobj); + handler(gobj, item.direction); + } else { + debug("not handling finalized", item.gobj); + } + + g_weak_ref_clear(&item.weak_ref); + } else { + debug("handle", item.gobj); + handler(item.gobj, item.direction); + } + + m_toggling_gobj = nullptr; return true; } @@ -128,30 +161,27 @@ ToggleQueue::enqueue(GObject *gobj, return; } - Item item{gobj, direction}; - /* If we're toggling up we take a reference to the object now, - * so it won't toggle down before we process it. This ensures we - * only ever have at most two toggle notifications queued. - * (either only up, or down-up) + if (is_being_handled(gobj)) + return; + + std::lock_guard hold(lock); + /* Only keep a weak reference on the object here, as if we're here, the + * JSObject wrapper has already a reference and we don't want to cause + * any weak notify in case it has lost one already in the main thread. + * So let's use the weak reference to keep track of the object till we + * don't handle this toggle. + * Unfortunately due to GNOME/glib#2384 we can't be symmetric here and + * behave differently on toggle up and down events, however when toggling + * up we already have a strong reference, so there's no much need to do */ + auto& item = q.emplace_back(gobj, direction); + if (direction == UP) { debug("enqueue UP", gobj); - g_object_ref(gobj); - item.needs_unref = true; } else { + g_weak_ref_init(&item.weak_ref, gobj); debug("enqueue DOWN", gobj); } - /* If we're toggling down, we don't need to take a reference since - * the associated JSObject already has one, and that JSObject won't - * get finalized until we've completed toggling (since it's rooted, - * until we unroot it when we dispatch the toggle down idle). - * - * Taking a reference now would be bad anyway, since it would force - * the object to toggle back up again. - */ - - std::lock_guard hold(lock); - q.push_back(item); if (m_idle_id) { g_assert(((void) "Should always enqueue with the same handler", diff --git a/gi/toggle.h b/gi/toggle.h index e77c5419a431dfa852999d4a426890daab3577e0..ae7228b8f86e23bdeb3201de2ab1bd41055215f6 100644 --- a/gi/toggle.h +++ b/gi/toggle.h @@ -31,17 +31,20 @@ public: private: struct Item { + Item() {} + Item(GObject* o, ToggleQueue::Direction d) : gobj(o), direction(d) {} GObject *gobj; + GWeakRef weak_ref; ToggleQueue::Direction direction; - unsigned needs_unref : 1; }; mutable std::mutex lock; std::deque q; std::atomic_bool m_shutdown = ATOMIC_VAR_INIT(false); - unsigned m_idle_id; - Handler m_toggle_handler; + unsigned m_idle_id = 0; + Handler m_toggle_handler = nullptr; + std::atomic m_toggling_gobj = nullptr; /* No-op unless GJS_VERBOSE_ENABLE_LIFECYCLE is defined to 1. */ inline void debug(const char* did GJS_USED_VERBOSE_LIFECYCLE, @@ -72,6 +75,10 @@ private: * want to wait for it to be processed in idle time. Returns false if queue * is empty. */ bool handle_toggle(Handler handler); + void handle_all_toggles(Handler handler); + + /* Checks if the gobj is currently being handled, to avoid recursion */ + bool is_being_handled(GObject* gobj); /* After calling this, the toggle queue won't accept any more toggles. Only * intended for use when destroying the JSContext and breaking the diff --git a/gi/value.cpp b/gi/value.cpp index 2b82d64d16277c7b303a33b632ecd0b31642aba6..836baa0e06191509b99b95ef89cc5b536dbdc341 100644 --- a/gi/value.cpp +++ b/gi/value.cpp @@ -842,18 +842,9 @@ gjs_value_from_g_value_internal(JSContext *context, v = g_value_get_boolean(gvalue); value_p.setBoolean(!!v); } else if (g_type_is_a(gtype, G_TYPE_OBJECT) || g_type_is_a(gtype, G_TYPE_INTERFACE)) { - GObject *gobj; - - gobj = (GObject*) g_value_get_object(gvalue); - - if (gobj) { - JSObject* obj = ObjectInstance::wrapper_from_gobject(context, gobj); - if (!obj) - return false; - value_p.setObject(*obj); - } else { - value_p.setNull(); - } + return ObjectInstance::set_value_from_gobject( + context, static_cast(g_value_get_object(gvalue)), + value_p); } else if (gtype == G_TYPE_STRV) { if (!gjs_array_from_strv (context, value_p, diff --git a/installed-tests/js/libgjstesttools/gjs-test-tools.cpp b/installed-tests/js/libgjstesttools/gjs-test-tools.cpp new file mode 100644 index 0000000000000000000000000000000000000000..276303af4a3ed9f395fc6d5409e804db80e8de63 --- /dev/null +++ b/installed-tests/js/libgjstesttools/gjs-test-tools.cpp @@ -0,0 +1,316 @@ +/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later +// SPDX-FileCopyrightText: 2021 Marco Trevisan + +#include "installed-tests/js/libgjstesttools/gjs-test-tools.h" + +#include +#include + +#include "gjs/jsapi-util.h" + +#ifdef G_OS_UNIX +# include +# include /* for FD_CLOEXEC */ +# include +# include /* for close, write */ + +# include /* for g_unix_open_pipe */ +#endif + +static GObject* m_tmp_object = NULL; +static GWeakRef m_tmp_weak; +static std::unordered_set m_finalized_objects; +static std::mutex m_finalized_objects_lock; + +struct FinalizedObjectsLocked { + FinalizedObjectsLocked() : hold(m_finalized_objects_lock) {} + + std::unordered_set* operator->() { return &m_finalized_objects; } + std::lock_guard hold; +}; + +void gjs_test_tools_init() {} + +void gjs_test_tools_reset() { + if (!FinalizedObjectsLocked()->count(m_tmp_object)) + g_clear_object(&m_tmp_object); + else + m_tmp_object = nullptr; + + g_weak_ref_set(&m_tmp_weak, nullptr); + + FinalizedObjectsLocked()->clear(); +} + +// clang-format off +static G_DEFINE_QUARK(gjs-test-utils::finalize, finalize); +// clang-format on + +static void monitor_object_finalization(GObject* object) { + g_object_steal_qdata(object, finalize_quark()); + g_object_set_qdata_full(object, finalize_quark(), object, [](void* data) { + FinalizedObjectsLocked()->insert(static_cast(data)); + }); +} + +void gjs_test_tools_delayed_ref(GObject* object, int interval) { + g_timeout_add( + interval, + [](void *data) { + g_object_ref(G_OBJECT(data)); + return G_SOURCE_REMOVE; + }, + object); +} + +void gjs_test_tools_delayed_unref(GObject* object, int interval) { + g_timeout_add( + interval, + [](void *data) { + g_object_unref(G_OBJECT(data)); + return G_SOURCE_REMOVE; + }, + object); +} + +void gjs_test_tools_delayed_dispose(GObject* object, int interval) { + g_timeout_add( + interval, + [](void *data) { + g_object_run_dispose(G_OBJECT(data)); + return G_SOURCE_REMOVE; + }, + object); +} + +void gjs_test_tools_save_object(GObject* object) { + g_assert(!m_tmp_object); + g_set_object(&m_tmp_object, object); + monitor_object_finalization(object); +} + +void gjs_test_tools_save_object_unreffed(GObject* object) { + g_assert(!m_tmp_object); + m_tmp_object = object; + monitor_object_finalization(object); +} + +void gjs_test_tools_ref_other_thread(GObject* object) { + // cppcheck-suppress leakNoVarFunctionCall + g_thread_join(g_thread_new("ref_object", g_object_ref, object)); +} + +typedef enum { + REF = 1 << 0, + UNREF = 1 << 1, +} RefType; + +typedef struct { + GObject* object; + RefType ref_type; + int delay; +} RefThreadData; + +static RefThreadData* ref_thread_data_new(GObject* object, int interval, + RefType ref_type) { + auto* ref_data = g_new(RefThreadData, 1); + + ref_data->object = object; + ref_data->delay = interval; + ref_data->ref_type = ref_type; + + monitor_object_finalization(object); + + return ref_data; +} + +static void* ref_thread_func(void* data) { + GjsAutoPointer ref_data = + static_cast(data); + + if (FinalizedObjectsLocked()->count(ref_data->object)) + return nullptr; + + if (ref_data->delay > 0) + g_usleep(ref_data->delay); + + if (FinalizedObjectsLocked()->count(ref_data->object)) + return nullptr; + + if (ref_data->ref_type & REF) + g_object_ref(ref_data->object); + + if (!(ref_data->ref_type & UNREF)) { + return ref_data->object; + } else if (ref_data->ref_type & REF) { + g_usleep(ref_data->delay); + + if (FinalizedObjectsLocked()->count(ref_data->object)) + return nullptr; + } + + if (ref_data->object != m_tmp_object) + g_object_steal_qdata(ref_data->object, finalize_quark()); + g_object_unref(ref_data->object); + return nullptr; +} + +void gjs_test_tools_unref_other_thread(GObject* object) { + // cppcheck-suppress leakNoVarFunctionCall + g_thread_join(g_thread_new("unref_object", ref_thread_func, + ref_thread_data_new(object, -1, UNREF))); +} + +void gjs_test_tools_delayed_ref_other_thread(GObject* object, int interval) { + g_thread_unref(g_thread_new("ref_object", ref_thread_func, + ref_thread_data_new(object, interval, REF))); +} + +void gjs_test_tools_delayed_unref_other_thread(GObject* object, int interval) { + g_thread_unref(g_thread_new("unref_object", ref_thread_func, + ref_thread_data_new(object, interval, UNREF))); +} + +void gjs_test_tools_delayed_ref_unref_other_thread(GObject* object, + int interval) { + g_thread_unref( + g_thread_new("ref_unref_object", ref_thread_func, + ref_thread_data_new(object, interval, + static_cast(REF | UNREF)))); +} + +void gjs_test_tools_run_dispose_other_thread(GObject* object) { + // cppcheck-suppress leakNoVarFunctionCall + g_thread_join(g_thread_new( + "run_dispose", + [](void* object) -> void* { + g_object_run_dispose(G_OBJECT(object)); + return nullptr; + }, + object)); +} + +/** + * gjs_test_tools_get_saved: + * Returns: (transfer full) + */ +GObject* gjs_test_tools_get_saved() { + if (FinalizedObjectsLocked()->count(m_tmp_object)) + m_tmp_object = nullptr; + + return static_cast(g_steal_pointer(&m_tmp_object)); +} + +/** + * gjs_test_tools_steal_saved: + * Returns: (transfer none) + */ +GObject* gjs_test_tools_steal_saved() { return gjs_test_tools_get_saved(); } + +void gjs_test_tools_save_weak(GObject* object) { + g_weak_ref_set(&m_tmp_weak, object); +} + +/** + * gjs_test_tools_peek_saved: + * Returns: (transfer none) + */ +GObject* gjs_test_tools_peek_saved() { + if (FinalizedObjectsLocked()->count(m_tmp_object)) + return nullptr; + + return m_tmp_object; +} + +/** + * gjs_test_tools_get_weak: + * Returns: (transfer full) + */ +GObject* gjs_test_tools_get_weak() { + return static_cast(g_weak_ref_get(&m_tmp_weak)); +} + +/** + * gjs_test_tools_get_weak_other_thread: + * Returns: (transfer full) + */ +GObject* gjs_test_tools_get_weak_other_thread() { + return static_cast( + // cppcheck-suppress leakNoVarFunctionCall + g_thread_join(g_thread_new( + "weak_get", + [](void*) -> void* { return gjs_test_tools_get_weak(); }, NULL))); +} + +/** + * gjs_test_tools_get_disposed: + * Returns: (transfer none) + */ +GObject* gjs_test_tools_get_disposed(GObject* object) { + g_object_run_dispose(G_OBJECT(object)); + return object; +} + +#ifdef G_OS_UNIX + +// Adapted from glnx_throw_errno_prefix() +static gboolean throw_errno_prefix(GError** error, const char* prefix) { + int errsv = errno; + + g_set_error_literal(error, G_IO_ERROR, g_io_error_from_errno(errsv), + g_strerror(errsv)); + g_prefix_error(error, "%s: ", prefix); + + errno = errsv; + return FALSE; +} + +#endif /* G_OS_UNIX */ + +/** + * gjs_open_bytes: + * @bytes: bytes to send to the pipe + * @error: Return location for a #GError, or %NULL + * + * Creates a pipe and sends @bytes to it, such that it is suitable for passing + * to g_subprocess_launcher_take_fd(). + * + * Returns: file descriptor, or -1 on error + */ +int gjs_test_tools_open_bytes(GBytes* bytes, GError** error) { + int pipefd[2], result; + size_t count; + const void* buf; + ssize_t bytes_written; + + g_return_val_if_fail(bytes, -1); + g_return_val_if_fail(error == NULL || *error == NULL, -1); + +#ifdef G_OS_UNIX + if (!g_unix_open_pipe(pipefd, FD_CLOEXEC, error)) + return -1; + + buf = g_bytes_get_data(bytes, &count); + + bytes_written = write(pipefd[1], buf, count); + if (bytes_written < 0) { + throw_errno_prefix(error, "write"); + return -1; + } + + if ((size_t)bytes_written != count) + g_warning("%s: %zu bytes sent, only %zd bytes written", __func__, count, + bytes_written); + + result = close(pipefd[1]); + if (result == -1) { + throw_errno_prefix(error, "close"); + return -1; + } + + return pipefd[0]; +#else + g_error("%s is currently supported on UNIX only", __func__); +#endif +} diff --git a/installed-tests/js/libgjstesttools/gjs-test-tools.h b/installed-tests/js/libgjstesttools/gjs-test-tools.h new file mode 100644 index 0000000000000000000000000000000000000000..9f692941e6b0e09249e7c5e49483195f597fd60e --- /dev/null +++ b/installed-tests/js/libgjstesttools/gjs-test-tools.h @@ -0,0 +1,56 @@ +/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later +// SPDX-FileCopyrightText: 2021 Marco Trevisan + +#pragma once + +#include +#include +#include + +G_BEGIN_DECLS + +void gjs_test_tools_init(void); + +void gjs_test_tools_reset(void); + +void gjs_test_tools_delayed_ref(GObject* object, int interval); + +void gjs_test_tools_delayed_unref(GObject* object, int interval); + +void gjs_test_tools_delayed_dispose(GObject* object, int interval); + +void gjs_test_tools_ref_other_thread(GObject* object); + +void gjs_test_tools_delayed_ref_other_thread(GObject* object, int interval); + +void gjs_test_tools_unref_other_thread(GObject* object); + +void gjs_test_tools_delayed_unref_other_thread(GObject* object, int interval); + +void gjs_test_tools_delayed_ref_unref_other_thread(GObject* object, + int interval); + +void gjs_test_tools_run_dispose_other_thread(GObject* object); + +void gjs_test_tools_save_object(GObject* object); + +void gjs_test_tools_save_object_unreffed(GObject* object); + +GObject* gjs_test_tools_get_saved(); + +GObject* gjs_test_tools_steal_saved(); + +GObject* gjs_test_tools_peek_saved(); + +void gjs_test_tools_save_weak(GObject* object); + +GObject* gjs_test_tools_get_weak(); + +GObject* gjs_test_tools_get_weak_other_thread(); + +GObject* gjs_test_tools_get_disposed(GObject* object); + +int gjs_test_tools_open_bytes(GBytes* bytes, GError** error); + +G_END_DECLS diff --git a/installed-tests/js/libgjstesttools/meson.build b/installed-tests/js/libgjstesttools/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..2371f6fd77807e66cb6cb109889c18067add4035 --- /dev/null +++ b/installed-tests/js/libgjstesttools/meson.build @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later +# SPDX-FileCopyrightText: 2021 Marco Trevisan + +gjstest_tools_sources = [ + 'gjs-test-tools.cpp', + 'gjs-test-tools.h', +] +libgjstesttools = library('gjstesttools', + gjstest_tools_sources, dependencies: [glib, gobject, gio], + include_directories: top_include, dependencies: libgjs_dep, + cpp_args: libgjs_cpp_args + test_gir_extra_c_args + test_gir_warning_c_args, + install: get_option('installed_tests'), install_dir: installed_tests_execdir) +gjstest_tools_gir = gnome.generate_gir(libgjstesttools, + includes: ['GObject-2.0', 'Gio-2.0'], sources: gjstest_tools_sources, + namespace: 'GjsTestTools', nsversion: '1.0', + symbol_prefix: 'gjs_test_tools_', extra_args: '--warn-error', + install: get_option('installed_tests'), install_dir_gir: false, + install_dir_typelib: installed_tests_execdir) +gjstest_tools_typelib = gjstest_tools_gir[1] diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build index 8026f903f7dd6e6b53870f3042280a4c2740d1d5..97f9cd07dfaf913cef96a4bdc79494b36cc0adfd 100644 --- a/installed-tests/js/meson.build +++ b/installed-tests/js/meson.build @@ -89,6 +89,8 @@ gimarshallingtests_gir = gnome.generate_gir(libgimarshallingtests, install_dir_typelib: installed_tests_execdir) gimarshallingtests_typelib = gimarshallingtests_gir[1] +subdir('libgjstesttools') + jasmine_tests = [ 'self', 'ByteArray', @@ -149,6 +151,7 @@ foreach test : jasmine_tests test(test, minijasmine, args: test_file, depends: [ gschemas_compiled, + gjstest_tools_typelib, gimarshallingtests_typelib, regress_typelib, warnlib_typelib, diff --git a/installed-tests/js/testGDBus.js b/installed-tests/js/testGDBus.js index 6464d3d1f0d44fd9e1c479894df3f1e77dd0b5f8..ae93cda208f37a7ce8a2cf912037c29e90abd63d 100644 --- a/installed-tests/js/testGDBus.js +++ b/installed-tests/js/testGDBus.js @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2008 litl, LLC const ByteArray = imports.byteArray; -const {Gio, GjsPrivate, GLib} = imports.gi; +const {Gio, GjsTestTools, GLib} = imports.gi; /* The methods list with their signatures. * @@ -240,14 +240,14 @@ class Test { } fdOut(bytes) { - const fd = GjsPrivate.open_bytes(bytes); + const fd = GjsTestTools.open_bytes(bytes); const fdList = Gio.UnixFDList.new_from_array([fd]); return [0, fdList]; } fdOut2Async([bytes], invocation) { GLib.idle_add(GLib.PRIORITY_DEFAULT, function () { - const fd = GjsPrivate.open_bytes(bytes); + const fd = GjsTestTools.open_bytes(bytes); const fdList = Gio.UnixFDList.new_from_array([fd]); invocation.return_value_with_unix_fd_list(new GLib.Variant('(h)', [0]), fdList); @@ -556,7 +556,7 @@ describe('Exported DBus object', function () { it('can call a remote method with a Unix FD', function (done) { const expectedBytes = ByteArray.fromString('some bytes'); - const fd = GjsPrivate.open_bytes(expectedBytes); + const fd = GjsTestTools.open_bytes(expectedBytes); const fdList = Gio.UnixFDList.new_from_array([fd]); proxy.fdInRemote(0, fdList, ([bytes], exc, outFdList) => { expect(exc).toBeNull(); @@ -568,7 +568,7 @@ describe('Exported DBus object', function () { it('can call an asynchronously implemented remote method with a Unix FD', function (done) { const expectedBytes = ByteArray.fromString('some bytes'); - const fd = GjsPrivate.open_bytes(expectedBytes); + const fd = GjsTestTools.open_bytes(expectedBytes); const fdList = Gio.UnixFDList.new_from_array([fd]); proxy.fdIn2Remote(0, fdList, ([bytes], exc, outFdList) => { expect(exc).toBeNull(); diff --git a/installed-tests/js/testGObjectDestructionAccess.js b/installed-tests/js/testGObjectDestructionAccess.js index 0b35d8596cad1d32362c3376a26245b7f51a0e4d..1ec25c14375c8dafb29f1296c9932635bf9eb48d 100644 --- a/installed-tests/js/testGObjectDestructionAccess.js +++ b/installed-tests/js/testGObjectDestructionAccess.js @@ -4,9 +4,8 @@ imports.gi.versions.Gtk = '3.0'; -const GLib = imports.gi.GLib; -const GObject = imports.gi.GObject; -const Gtk = imports.gi.Gtk; +const {GLib, Gio, GjsTestTools, GObject, Gtk} = imports.gi; +const {system: System} = imports; describe('Access to destroyed GObject', function () { let destroyedWindow; @@ -40,6 +39,28 @@ describe('Access to destroyed GObject', function () { 'testExceptionInDestroyedObjectPropertySet'); }); + it('Add expando property', function () { + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + 'Object Gtk.Window (0x*'); + + destroyedWindow.expandoProperty = 'Hello!'; + + GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + 'testExceptionInDestroyedObjectExpandoPropertySet'); + }); + + it('Access to unset expando property', function () { + expect(destroyedWindow.expandoProperty).toBeUndefined(); + }); + + it('Access previously set expando property', function () { + destroyedWindow = new Gtk.Window({type: Gtk.WindowType.TOPLEVEL}); + destroyedWindow.expandoProperty = 'Hello!'; + destroyedWindow.destroy(); + + expect(destroyedWindow.expandoProperty).toBe('Hello!'); + }); + it('Access to getter method', function () { GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, 'Object Gtk.Window (0x*'); @@ -126,7 +147,7 @@ describe('Access to destroyed GObject', function () { it('Proto function toString', function () { expect(destroyedWindow.toString()).toMatch( - /\[object \(FINALIZED\) instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + /\[object \(DISPOSED\) instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); }); it('Proto function toString before/after', function () { @@ -138,6 +159,307 @@ describe('Access to destroyed GObject', function () { validWindow.destroy(); expect(validWindow.toString()).toMatch( - /\[object \(FINALIZED\) instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + /\[object \(DISPOSED\) instance wrapper GIName:Gtk.Window jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + }); +}); + +describe('Disposed or finalized GObject', function () { + beforeAll(function () { + GjsTestTools.init(); + }); + + afterEach(function () { + GjsTestTools.reset(); + }); + + [true, false].forEach(gc => { + it(`is marked as disposed when it is a manually disposed property ${gc ? '' : 'not '}garbage collected`, function () { + const emblem = new Gio.EmblemedIcon({ + gicon: new Gio.ThemedIcon({name: 'alarm'}), + }); + + let {gicon} = emblem; + gicon.run_dispose(); + gicon = null; + System.gc(); + + Array(10).fill().forEach(() => { + // We need to repeat the test to ensure that we disassociate + // wrappers from disposed objects on destruction. + gicon = emblem.gicon; + expect(gicon.toString()).toMatch( + /\[object \(DISPOSED\) instance wrapper .* jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + + gicon = null; + if (gc) + System.gc(); + }); + }); + }); + + it('calls dispose vfunc on explicit disposal only', function () { + const callSpy = jasmine.createSpy('vfunc_dispose'); + const DisposeFile = GObject.registerClass(class DisposeFile extends Gio.ThemedIcon { + vfunc_dispose(...args) { + expect(this.names).toEqual(['dummy']); + callSpy(...args); + } + }); + + let file = new DisposeFile({name: 'dummy'}); + file.run_dispose(); + expect(callSpy).toHaveBeenCalledOnceWith(); + + file.run_dispose(); + expect(callSpy).toHaveBeenCalledTimes(2); + file = null; + + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*during garbage collection*'); + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*dispose*'); + System.gc(); + GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + 'calls dispose vfunc on explicit disposal only'); + + expect(callSpy).toHaveBeenCalledTimes(2); + }); + + it('generates a warn on object garbage collection', function () { + Gio.File.new_for_path('/').unref(); + + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*Object 0x* has been finalized *'); + System.gc(); + GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + 'generates a warn on object garbage collection'); + }); + + it('generates a warn on object garbage collection if has expando property', function () { + let file = Gio.File.new_for_path('/'); + file.toggleReferenced = true; + file.unref(); + expect(file.toString()).toMatch( + /\[object \(FINALIZED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + file = null; + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*Object 0x* has been finalized *'); + System.gc(); + GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + 'generates a warn on object garbage collection if has expando property'); + }); + + it('generates a warn if already disposed at garbage collection', function () { + const loop = new GLib.MainLoop(null, false); + + let file = Gio.File.new_for_path('/'); + GjsTestTools.delayed_unref(file, 1); // Will happen after dispose + file.run_dispose(); + + let done = false; + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => (done = true)); + while (!done) + loop.get_context().iteration(true); + + file = null; + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*Object 0x* has been finalized *'); + System.gc(); + GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + 'generates a warn if already disposed at garbage collection'); + }); + + [true, false].forEach(gc => { + it(`created from other function is marked as disposed and ${gc ? '' : 'not '}garbage collected`, function () { + let file = Gio.File.new_for_path('/'); + GjsTestTools.save_object(file); + file.run_dispose(); + file = null; + System.gc(); + + Array(10).fill().forEach(() => { + // We need to repeat the test to ensure that we disassociate + // wrappers from disposed objects on destruction. + expect(GjsTestTools.peek_saved()).toMatch( + /\[object \(DISPOSED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + if (gc) + System.gc(); + }); + }); + }); + + it('returned from function is marked as disposed', function () { + expect(GjsTestTools.get_disposed(Gio.File.new_for_path('/'))).toMatch( + /\[object \(DISPOSED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + }); + + it('returned from function is marked as disposed and then as finalized', function () { + let file = Gio.File.new_for_path('/'); + GjsTestTools.save_object(file); + GjsTestTools.delayed_unref(file, 30); + file.run_dispose(); + + let disposedFile = GjsTestTools.get_saved(); + expect(disposedFile).toEqual(file); + expect(disposedFile).toMatch( + /\[object \(DISPOSED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + + file = null; + System.gc(); + + const loop = new GLib.MainLoop(null, false); + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => loop.quit()); + loop.run(); + + expect(disposedFile).toMatch( + /\[object \(FINALIZED\) instance wrapper GType:GLocalFile jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/); + + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*Object 0x* has been finalized *'); + disposedFile = null; + System.gc(); + GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectDestructionAccess.js', 0, + 'returned from function is marked as disposed and then as finalized'); + }); + + it('ignores toggling queued unref toggles', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + file.ref(); + GjsTestTools.unref_other_thread(file); + file.run_dispose(); + }); + + it('ignores toggling queued toggles', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + GjsTestTools.ref_other_thread(file); + GjsTestTools.unref_other_thread(file); + file.run_dispose(); + }); + + it('can be disposed from other thread', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + file.ref(); + GjsTestTools.unref_other_thread(file); + GjsTestTools.run_dispose_other_thread(file); + }); + + it('can be garbage collected once disposed from other thread', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + GjsTestTools.run_dispose_other_thread(file); + file = null; + System.gc(); + }); + + it('can be re-reffed from other thread delayed', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + const objectAddress = System.addressOfGObject(file); + GjsTestTools.save_object_unreffed(file); + GjsTestTools.delayed_ref_other_thread(file, 10); + file = null; + System.gc(); + + const loop = new GLib.MainLoop(null, false); + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => loop.quit()); + loop.run(); + + // We need to cleanup the extra ref we added before now. + // However, depending on whether the thread ref happens the object + // may be already finalized, and in such case we need to throw + try { + file = GjsTestTools.steal_saved(); + if (file) { + expect(System.addressOfGObject(file)).toBe(objectAddress); + expect(file instanceof Gio.File).toBeTruthy(); + file.unref(); + } + } catch (e) { + expect(() => { + throw e; + }).toThrowError(/.*Unhandled GType.*/); + } + }); + + it('can be re-reffed and unreffed again from other thread', function () { + let file = Gio.File.new_for_path('/'); + const objectAddress = System.addressOfGObject(file); + file.expandMeWithToggleRef = true; + GjsTestTools.save_object(file); + GjsTestTools.delayed_unref_other_thread(file.ref(), 10); + file = null; + System.gc(); + + const loop = new GLib.MainLoop(null, false); + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => loop.quit()); + loop.run(); + + file = GjsTestTools.get_saved(); + expect(System.addressOfGObject(file)).toBe(objectAddress); + expect(file instanceof Gio.File).toBeTruthy(); + }); + + it('can be re-reffed and unreffed again from other thread with delay', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + GjsTestTools.delayed_ref_unref_other_thread(file, 10); + file = null; + System.gc(); + + const loop = new GLib.MainLoop(null, false); + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => loop.quit()); + loop.run(); + }); + + it('can be toggled up by getting a GWeakRef', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + GjsTestTools.save_weak(file); + GjsTestTools.get_weak(); + }); + + it('can be toggled up by getting a GWeakRef from another thread', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + GjsTestTools.save_weak(file); + GjsTestTools.get_weak_other_thread(); + }); + + it('can be toggled up by getting a GWeakRef from another thread and re-reffed in main thread', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + GjsTestTools.save_weak(file); + GjsTestTools.get_weak_other_thread(); + + // Ok, let's play more dirty now... + file.ref(); // toggle up + file.unref(); // toggle down + + file.ref(); + file.ref(); + file.unref(); + file.unref(); + }); + + it('can be toggled up by getting a GWeakRef from another and re-reffed from various threads', function () { + let file = Gio.File.new_for_path('/'); + file.expandMeWithToggleRef = true; + GjsTestTools.save_weak(file); + GjsTestTools.get_weak_other_thread(); + + GjsTestTools.ref_other_thread(file); + GjsTestTools.unref_other_thread(file); + + file.ref(); + file.unref(); + + GjsTestTools.ref_other_thread(file); + file.unref(); + + file.ref(); + GjsTestTools.unref_other_thread(file); }); }); diff --git a/installed-tests/js/testGtk3.js b/installed-tests/js/testGtk3.js index fd0608d1d15db0ff9162a58fabf6572f00fc0722..a6ba26426d1df763f06b049b9a965e605598e6dc 100644 --- a/installed-tests/js/testGtk3.js +++ b/installed-tests/js/testGtk3.js @@ -191,19 +191,44 @@ describe('Gtk overrides', function () { 'Gtk overrides avoid crashing and print a stack trace'); }); - it('GTK vfuncs can be explicitly called during disposition', function () { - let called; - const GoodLabel = GObject.registerClass(class GoodLabel extends Gtk.Label { + it('GTK vfuncs are not called if the object is disposed', function () { + const spy = jasmine.createSpy('vfunc_destroy'); + const NotSoGoodLabel = GObject.registerClass(class NotSoGoodLabel extends Gtk.Label { vfunc_destroy() { - called = true; + spy(); } }); - let label = new GoodLabel(); + let label = new NotSoGoodLabel(); + label.destroy(); - expect(called).toBeTruthy(); + expect(spy).toHaveBeenCalledTimes(1); + + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*during garbage collection*'); + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + '*destroy*'); label = null; System.gc(); + GLib.test_assert_expected_messages_internal('Gjs', 'testGtk3.js', 0, + 'GTK vfuncs are not called if the object is disposed'); + }); + + it('destroy signal is emitted while disposing objects', function () { + const label = new Gtk.Label({label: 'Hello'}); + const handleDispose = jasmine.createSpy('handleDispose').and.callFake(() => { + expect(label.label).toBe('Hello'); + }); + label.connect('destroy', handleDispose); + label.destroy(); + + expect(handleDispose).toHaveBeenCalledWith(label); + + GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL, + 'Object Gtk.Label (0x* deallocated *'); + expect(label.label).toBeUndefined(); + GLib.test_assert_expected_messages_internal('Gjs', 'testGtk3.js', 0, + 'GTK destroy signal is emitted while disposing objects'); }); it('accepts string in place of GdkAtom', function () { diff --git a/libgjs-private/gjs-util.c b/libgjs-private/gjs-util.c index 4fd268cd0cccad2ef5d445766e75ff74b76d1a5f..9dbe5f6f11eaf4b9c2506f251173f8ecb40c9b0c 100644 --- a/libgjs-private/gjs-util.c +++ b/libgjs-private/gjs-util.c @@ -9,21 +9,11 @@ #include /* for setlocale */ #include /* for size_t */ -#include #include #include #include #include /* for bindtextdomain, bind_textdomain_codeset, textdomain */ -#ifdef G_OS_UNIX -# include -# include /* for FD_CLOEXEC */ -# include -# include /* for close, write */ - -# include /* for g_unix_open_pipe */ -#endif - #include "libgjs-private/gjs-util.h" char * @@ -108,88 +98,6 @@ gjs_param_spec_get_owner_type(GParamSpec *pspec) return pspec->owner_type; } -#ifdef G_OS_UNIX - -// Adapted from glnx_throw_errno_prefix() -G_GNUC_PRINTF(2, 3) -static gboolean throw_errno_prefix(GError** error, const char* fmt, ...) { - int errsv = errno; - char* old_msg; - GString* buf; - - va_list args; - - if (!error) - return FALSE; - - va_start(args, fmt); - - g_set_error_literal(error, G_IO_ERROR, g_io_error_from_errno(errsv), - g_strerror(errsv)); - - old_msg = g_steal_pointer(&(*error)->message); - buf = g_string_new(""); - g_string_append_vprintf(buf, fmt, args); - g_string_append(buf, ": "); - g_string_append(buf, old_msg); - g_free(old_msg); - (*error)->message = g_string_free(g_steal_pointer(&buf), FALSE); - - va_end(args); - - errno = errsv; - return FALSE; -} - -#endif /* G_OS_UNIX */ - -/** - * gjs_open_bytes: - * @bytes: bytes to send to the pipe - * @error: Return location for a #GError, or %NULL - * - * Creates a pipe and sends @bytes to it, such that it is suitable for passing - * to g_subprocess_launcher_take_fd(). - * - * Returns: file descriptor, or -1 on error - */ -int gjs_open_bytes(GBytes* bytes, GError** error) { - int pipefd[2], result; - size_t count; - const void* buf; - ssize_t bytes_written; - - g_return_val_if_fail(bytes, -1); - g_return_val_if_fail(error == NULL || *error == NULL, -1); - -#ifdef G_OS_UNIX - if (!g_unix_open_pipe(pipefd, FD_CLOEXEC, error)) - return -1; - - buf = g_bytes_get_data(bytes, &count); - - bytes_written = write(pipefd[1], buf, count); - if (bytes_written < 0) { - throw_errno_prefix(error, "write"); - return -1; - } - - if ((size_t)bytes_written != count) - g_warning("%s: %zu bytes sent, only %zd bytes written", __func__, count, - bytes_written); - - result = close(pipefd[1]); - if (result == -1) { - throw_errno_prefix(error, "close"); - return -1; - } - - return pipefd[0]; -#else - g_error("%s is currently supported on UNIX only", __func__); -#endif -} - static GParamSpec* gjs_gtk_container_class_find_child_property( GIObjectInfo* container_info, GObject* container, const char* property) { GIBaseInfo* class_info = NULL; diff --git a/libgjs-private/gjs-util.h b/libgjs-private/gjs-util.h index d4f8bbf6cc1ceab6f5a10bf6619f746917524d85..c24f23590f4932ac425305ff8ce51b22af602d3f 100644 --- a/libgjs-private/gjs-util.h +++ b/libgjs-private/gjs-util.h @@ -57,10 +57,6 @@ void gjs_gtk_container_child_set_property(GObject* container, GObject* child, const char* property, const GValue* value); -/* For tests */ -GJS_EXPORT -int gjs_open_bytes(GBytes* bytes, GError** error); - G_END_DECLS #endif /* LIBGJS_PRIVATE_GJS_UTIL_H_ */ diff --git a/meson.build b/meson.build index 33a3389d64ed0a23d55fcc79a1d591408a740a7e..272f2033414189b427d699948210d86a6ebc8bf5 100644 --- a/meson.build +++ b/meson.build @@ -577,15 +577,16 @@ pkg.generate(libgjs, name: api_name, description: 'JS bindings for GObjects', tests_environment = environment() js_tests_builddir = meson.current_build_dir() / 'installed-tests' / 'js' +libgjs_test_tools_builddir = js_tests_builddir / 'libgjstesttools' # GJS_PATH is empty here since we want to force the use of our own # resources. G_FILENAME_ENCODING ensures filenames are not UTF-8 tests_environment.set('TOP_BUILDDIR', meson.build_root()) tests_environment.set('GJS_USE_UNINSTALLED_FILES', '1') tests_environment.set('GJS_PATH', '') tests_environment.prepend('GI_TYPELIB_PATH', meson.current_build_dir(), - js_tests_builddir) + js_tests_builddir, libgjs_test_tools_builddir) tests_environment.prepend('LD_LIBRARY_PATH', meson.current_build_dir(), - js_tests_builddir) + js_tests_builddir, libgjs_test_tools_builddir) tests_environment.set('G_FILENAME_ENCODING', 'latin1') # Workaround for https://github.com/google/sanitizers/issues/1322 tests_environment.set('ASAN_OPTIONS', 'intercept_tls_get_addr=0')