[gjs/wip/ptomato/mozjs38: 3/29] js: Refactor dual use of JS::Heap wrapper



commit 332da40e969b9bb9a7b98392be811e5d8ea39437
Author: Philip Chimento <philip chimento gmail com>
Date:   Wed Jan 11 23:34:40 2017 -0800

    js: Refactor dual use of JS::Heap wrapper
    
    The previous situation was that a JS::Heap wrapper was used to root a GC
    thing under some conditions using JS::AddFooRoot() or the keep-alive
    object, and trace or maintain a weak pointer otherwise.
    
    This will not be possible anymore in SpiderMonkey 38. JS::AddFooRoot()
    and JS::RemoveFooRoot() are going away, in favour of
    JS::PersistentRootedFoo. The keep-alive object has its own problems,
    because the SpiderMonkey 38 garbage collector will move GC things around,
    so anything referring to a GC thing on the keep-alive object will have to
    know when to update its JS::Heap wrapper when the GC thing moves.
    
    This previous situation existed in two places.
    
      (1) The JS::Value holding a trampoline's function. If the function was
      owned by the trampoline (the normal case), then it was rooted. If the
      function was a vfunc, in which case it was owned by the GObject class
      prototype and the trampoline was essentially leaked, then it remained a
      weak pointer.
    
      (2) The JSObject associated with a closure. In the normal case the
      object and the closure had the same lifetime and the object was rooted.
      Similar to above, if the closure was a signal it was owned by the
      GObject class, and traced.
    
    For both of these places we now use GjsMaybeOwned, a wrapper that sticks
    its GC thing in either a JS::PersistentRootedFoo, if the thing is
    intended to be rooted, or a JS::Heap<Foo>, if it is not. If rooted, the
    GjsMaybeOwned holds a weak reference to the GjsContext, and therefore
    can send out a notification when the context's dispose function is run,
    similar to existing functionality of the keep-alive object.
    
    This will still need to change further after the switch to SpiderMonkey
    38, since weak pointers must be updated when they are moved by the GC.
    (This is technically already the case in SpiderMonkey 31, but the API
    makes it difficult to do correctly, and in practice it isn't necessary.)
    
    https://bugzilla.gnome.org/show_bug.cgi?id=776966

 Makefile-test.am          |    1 +
 Makefile.am               |    1 +
 gi/closure.cpp            |   37 +++----
 gi/function.cpp           |   17 +--
 gi/function.h             |    5 +-
 gjs/context.cpp           |    6 +-
 gjs/jsapi-util-root.h     |  273 +++++++++++++++++++++++++++++++++++++++++++++
 test/gjs-test-rooting.cpp |  237 +++++++++++++++++++++++++++++++++++++++
 test/gjs-test-utils.cpp   |   15 ++-
 test/gjs-test-utils.h     |    9 ++
 test/gjs-tests.cpp        |    1 +
 11 files changed, 563 insertions(+), 39 deletions(-)
---
diff --git a/Makefile-test.am b/Makefile-test.am
index b4641db..ff3ff33 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -100,6 +100,7 @@ gjs_tests_SOURCES =                                 \
        test/gjs-test-utils.h                           \
        test/gjs-test-call-args.cpp                     \
        test/gjs-test-coverage.cpp                      \
+       test/gjs-test-rooting.cpp                       \
        mock-js-resources.c                             \
        $(NULL)
 
diff --git a/Makefile.am b/Makefile.am
index 91cc833..c6a2f60 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -90,6 +90,7 @@ libgjs_la_SOURCES =           \
        gjs/jsapi-dynamic-class.cpp \
        gjs/jsapi-util-args.h           \
        gjs/jsapi-util-error.cpp        \
+       gjs/jsapi-util-root.h           \
        gjs/jsapi-util-string.cpp       \
        gjs/mem.cpp             \
        gjs/native.cpp          \
diff --git a/gi/closure.cpp b/gi/closure.cpp
index 3747296..59c69f6 100644
--- a/gi/closure.cpp
+++ b/gi/closure.cpp
@@ -28,14 +28,14 @@
 #include <util/log.h>
 
 #include "closure.h"
+#include "gjs/jsapi-util-root.h"
 #include "gjs/jsapi-wrapper.h"
 #include "gjs/mem.h"
-#include "keep-alive.h"
 
 struct Closure {
     JSRuntime *runtime;
     JSContext *context;
-    JS::Heap<JSObject *> obj;
+    GjsMaybeOwned<JSObject *> obj;
     guint unref_on_global_object_finalized : 1;
 };
 
@@ -91,7 +91,7 @@ invalidate_js_pointers(GjsClosure *gc)
     if (c->obj == NULL)
         return;
 
-    c->obj = NULL;
+    c->obj.reset();
     c->context = NULL;
     c->runtime = NULL;
 
@@ -102,8 +102,8 @@ invalidate_js_pointers(GjsClosure *gc)
 }
 
 static void
-global_context_finalized(JSObject *obj,
-                         void     *data)
+global_context_finalized(JS::HandleObject obj,
+                         void            *data)
 {
     GjsClosure *gc = (GjsClosure*) data;
     Closure *c;
@@ -113,7 +113,7 @@ global_context_finalized(JSObject *obj,
 
     gjs_debug_closure("Context global object destroy notifier on closure %p "
                       "which calls object %p",
-                      c, c->obj);
+                      c, c->obj.get());
 
     /* invalidate_js_pointers() could free us so check flag now to avoid
      * invalid memory access
@@ -152,7 +152,7 @@ check_context_valid(GjsClosure *gc)
 
     gjs_debug_closure("Context %p no longer exists, invalidating "
                       "closure %p which calls object %p",
-                      c->context, c, c->obj);
+                      c->context, c, c->obj.get());
 
     /* Did not find the context. */
     invalidate_js_pointers(gc);
@@ -179,7 +179,7 @@ closure_invalidated(gpointer data,
 
     GJS_DEC_COUNTER(closure);
     gjs_debug_closure("Invalidating closure %p which calls object %p",
-                      closure, c->obj);
+                      closure, c->obj.get());
 
     if (c->obj == NULL) {
         gjs_debug_closure("   (closure %p already dead, nothing to do)",
@@ -234,12 +234,8 @@ closure_invalidated(gpointer data,
         gjs_debug_closure("   (closure %p's context was alive, "
                           "removing our destroy notifier on global object)",
                           closure);
-        gjs_keep_alive_remove_global_child(c->context,
-                                           global_context_finalized,
-                                           c->obj,
-                                           closure);
 
-        c->obj = NULL;
+        c->obj.reset();
         c->context = NULL;
         c->runtime = NULL;
     }
@@ -251,7 +247,7 @@ closure_set_invalid(gpointer  data,
 {
     Closure *self = &((GjsClosure*) closure)->priv;
 
-    self->obj = NULL;
+    self->obj.reset();
     self->context = NULL;
     self->runtime = NULL;
 
@@ -303,7 +299,7 @@ gjs_closure_invoke(GClosure                   *closure,
         /* Exception thrown... */
         gjs_debug_closure("Closure invocation failed (exception should "
                           "have been thrown) closure %p callable %p",
-                          closure, c->obj);
+                          closure, c->obj.get());
         if (!gjs_log_exception(context))
             gjs_debug_closure("Closure invocation failed but no exception was set?"
                               "closure %p", closure);
@@ -360,7 +356,7 @@ gjs_closure_trace(GClosure *closure,
     if (c->obj == NULL)
         return;
 
-    JS_CallHeapObjectTracer(tracer, &c->obj, "signal connection");
+    c->obj.trace(tracer, "signal connection");
 }
 
 GClosure*
@@ -384,20 +380,17 @@ gjs_closure_new(JSContext  *context,
     c->context = context;
     JS_BeginRequest(context);
 
-    c->obj = callable;
     c->unref_on_global_object_finalized = false;
 
     GJS_INC_COUNTER(closure);
 
     if (root_function) {
         /* Fully manage closure lifetime if so asked */
-        gjs_keep_alive_add_global_child(context,
-                                        global_context_finalized,
-                                        c->obj,
-                                        gc);
+        c->obj.root(context, callable, global_context_finalized, gc);
 
         g_closure_add_invalidate_notifier(&gc->base, NULL, closure_invalidated);
     } else {
+        c->obj = callable;
         /* Only mark the closure as invalid if memory is managed
            outside (i.e. by object.c for signals) */
         g_closure_add_invalidate_notifier(&gc->base, NULL, closure_set_invalid);
@@ -406,7 +399,7 @@ gjs_closure_new(JSContext  *context,
     g_closure_add_finalize_notifier(&gc->base, NULL, closure_finalize);
 
     gjs_debug_closure("Create closure %p which calls object %p '%s'",
-                      gc, c->obj, description);
+                      gc, c->obj.get(), description);
 
     JS_EndRequest(context);
 
diff --git a/gi/function.cpp b/gi/function.cpp
index 0c564c4..07d4783 100644
--- a/gi/function.cpp
+++ b/gi/function.cpp
@@ -82,14 +82,6 @@ gjs_callback_trampoline_unref(GjsCallbackTrampoline *trampoline)
 
     trampoline->ref_count--;
     if (trampoline->ref_count == 0) {
-        JSContext *context = trampoline->context;
-
-        if (!trampoline->is_vfunc) {
-            JS_BeginRequest(context);
-            JS::RemoveValueRoot(context, &trampoline->js_function);
-            JS_EndRequest(context);
-        }
-
         g_callable_info_free_closure(trampoline->info, trampoline->closure);
         g_base_info_unref( (GIBaseInfo*) trampoline->info);
         g_free (trampoline->param_types);
@@ -208,7 +200,7 @@ gjs_callback_closure(ffi_cif *cif,
     }
 
     JS_BeginRequest(context);
-    func_obj = &trampoline->js_function.toObject();
+    func_obj = &trampoline->js_function.get().toObject();
     JSAutoCompartment ac(context, func_obj);
 
     n_args = g_callable_info_get_n_args(trampoline->info);
@@ -455,9 +447,10 @@ gjs_callback_trampoline_new(JSContext      *context,
     trampoline->context = context;
     trampoline->info = callable_info;
     g_base_info_ref((GIBaseInfo*)trampoline->info);
-    trampoline->js_function = function;
-    if (!is_vfunc)
-        JS::AddValueRoot(context, &trampoline->js_function);
+    if (is_vfunc)
+        trampoline->js_function = function;
+    else
+        trampoline->js_function.root(context, function);
 
     /* Analyze param types and directions, similarly to init_cached_function_data */
     n_args = g_callable_info_get_n_args(trampoline->info);
diff --git a/gi/function.h b/gi/function.h
index 8bc9cb1..4c0df03 100644
--- a/gi/function.h
+++ b/gi/function.h
@@ -28,6 +28,7 @@
 #include <glib.h>
 
 #include "gjs/jsapi-util.h"
+#include "gjs/jsapi-util-root.h"
 
 #include <girepository.h>
 #include <girffi.h>
@@ -45,7 +46,9 @@ struct GjsCallbackTrampoline {
     gint ref_count;
     JSContext *context;
     GICallableInfo *info;
-    JS::Heap<JS::Value> js_function;
+
+    GjsMaybeOwned<JS::Value> js_function;
+
     ffi_cif cif;
     ffi_closure *closure;
     GIScopeType scope;
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 222a178..a75c938 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -391,6 +391,10 @@ gjs_context_dispose(GObject *object)
 
     js_context = GJS_CONTEXT(object);
 
+    /* Run dispose notifications first, so that anything releasing
+     * references in response to this can still get garbage collected */
+    G_OBJECT_CLASS(gjs_context_parent_class)->dispose(object);
+
     if (js_context->context != NULL) {
 
         gjs_debug(GJS_DEBUG_CONTEXT,
@@ -430,8 +434,6 @@ gjs_context_dispose(GObject *object)
         js_context->context = NULL;
         g_clear_pointer(&js_context->runtime, gjs_runtime_unref);
     }
-
-    G_OBJECT_CLASS(gjs_context_parent_class)->dispose(object);
 }
 
 static void
diff --git a/gjs/jsapi-util-root.h b/gjs/jsapi-util-root.h
new file mode 100644
index 0000000..9cc12ea
--- /dev/null
+++ b/gjs/jsapi-util-root.h
@@ -0,0 +1,273 @@
+/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (c) 2017 Endless Mobile, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#ifndef GJS_JSAPI_UTIL_ROOT_H
+#define GJS_JSAPI_UTIL_ROOT_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "gjs/context.h"
+#include "gjs/jsapi-wrapper.h"
+#include "util/log.h"
+
+/* jsapi-util-root.h - Utilities for dealing with the lifetime and ownership of
+ * JS Objects and other things that can be collected by the garbage collector
+ * (collectively called "GC things.")
+ *
+ * GjsMaybeOwned<T> is a multi-purpose wrapper for a GC thing of type T. You can
+ * wrap a thing in one of three ways:
+ *
+ * - trace the thing (tie it to the lifetime of another GC thing),
+ * - root the thing (keep it alive as long as the wrapper is in existence),
+ * - maintain a weak pointer to the thing (not keep it alive at all and have it
+ *   possibly be finalized out from under you).
+ *
+ * To trace or maintain a weak pointer, simply assign a thing of type T to the
+ * GjsMaybeOwned wrapper. For tracing, you must call the trace() method when
+ * your other GC thing is traced.
+ *
+ * Rooting requires a JSContext so can't just assign a thing of type T. Instead
+ * you need to call the root() method to set up rooting.
+ *
+ * If the thing is rooted, it will be unrooted either when the GjsMaybeOwned is
+ * destroyed, or when the JSContext is destroyed. In the latter case, you can
+ * get an optional notification by passing a callback to root().
+ *
+ * To switch between one of the three modes, you must first call reset(). This
+ * drops all references to any GC thing and leaves the GjsMaybeOwned in the
+ * same state as if it had just been constructed.
+ */
+
+/* This struct contains operations that must be implemented differently
+ * depending on the type of the GC thing. Add more types as necessary. If an
+ * implementation is never used, it's OK to leave it out. The compiler will
+ * complain if it's used somewhere but not instantiated here.
+ */
+template<typename T>
+struct GjsHeapOperation {
+    static void trace(JSTracer    *tracer,
+                      JS::Heap<T> *thing,
+                      const char  *name);
+};
+
+template<>
+struct GjsHeapOperation<JSObject *> {
+    static void
+    trace(JSTracer             *tracer,
+          JS::Heap<JSObject *> *thing,
+          const char           *name)
+    {
+        JS_CallHeapObjectTracer(tracer, thing, name);
+    }
+};
+
+/* GjsMaybeOwned is intended only for use in heap allocation. Do not allocate it
+ * on the stack, and do not allocate any instances of structures that have it as
+ * a member on the stack either. Unfortunately we cannot enforce this at compile
+ * time with a private constructor; that would prevent the intended usage as a
+ * member of a heap-allocated struct. */
+template<typename T>
+class GjsMaybeOwned {
+public:
+    typedef void (*DestroyNotify)(JS::Handle<T> thing, void *data);
+
+private:
+    bool m_rooted;  /* wrapper is in rooted mode */
+    bool m_has_weakref;  /* we have a weak reference to the GjsContext */
+
+    JSContext *m_cx;
+    JS::Heap<T> m_heap;  /* should be untouched if in rooted mode */
+    JS::PersistentRooted<T> *m_root;  /* should be null if not in rooted mode */
+
+    DestroyNotify m_notify;
+    void *m_data;
+
+    /* No-op unless GJS_VERBOSE_ENABLE_LIFECYCLE is defined to 1. */
+    inline void
+    debug(const char *what)
+    {
+        gjs_debug_lifecycle(GJS_DEBUG_KEEP_ALIVE, "GjsMaybeOwned %p %s", this,
+                            what);
+    }
+
+    static void
+    on_context_destroy(void    *data,
+                       GObject *ex_context)
+    {
+        auto self = static_cast<GjsMaybeOwned<T> *>(data);
+        self->invalidate();
+    }
+
+    void
+    teardown_rooting(void)
+    {
+        debug("teardown_rooting()");
+        g_assert(m_rooted);
+
+        delete m_root;
+        m_root = nullptr;
+        m_rooted = false;
+
+        if (!m_has_weakref)
+            return;
+
+        auto gjs_cx = static_cast<GjsContext *>(JS_GetContextPrivate(m_cx));
+        g_object_weak_unref(G_OBJECT(gjs_cx), on_context_destroy, this);
+        m_has_weakref = false;
+    }
+
+    /* Called for a rooted wrapper when the JSContext is about to be destroyed.
+     * This calls the destroy-notify callback if one was passed to root(), and
+     * then removes all rooting from the object. */
+    void
+    invalidate(void)
+    {
+        debug("invalidate()");
+        g_assert(m_rooted);
+
+        /* The weak ref is already gone because the context is dead, so no need
+         * to remove it. */
+        m_has_weakref = false;
+
+        /* The object is still live across this callback. Re-entry into the
+         * destructor from the callback should also be safe. */
+        if (m_notify)
+            m_notify(handle(), m_data);
+
+        /* If the callback destroyed this wrapper already, we're done. */
+        if (!m_rooted)
+            return;
+
+        reset();
+    }
+
+public:
+    GjsMaybeOwned(void) :
+        m_rooted(false),
+        m_has_weakref(false),
+        m_cx(nullptr),
+        m_root(nullptr),
+        m_notify(nullptr),
+        m_data(nullptr)
+    {
+        debug("created");
+    }
+
+    ~GjsMaybeOwned(void)
+    {
+        debug("destroyed");
+        if (m_rooted)
+            teardown_rooting();
+    }
+
+    /* To access the GC thing, call get(). In many cases you can just use the
+     * GjsMaybeOwned wrapper in place of the GC thing itself due to the implicit
+     * cast operator. But if you want to call methods on the GC thing, for
+     * example if it's a JS::Value, you have to use get(). */
+    const T
+    get(void) const
+    {
+        return m_rooted ? m_root->get() : m_heap.get();
+    }
+    operator const T(void) const { return get(); }
+
+    bool
+    operator==(const T& other) const
+    {
+        if (m_rooted)
+            return m_root->get() == other;
+        return m_heap == other;
+    }
+    inline bool operator!=(const T& other) const { return !(*this == other); }
+
+    /* You can get a Handle<T> if the thing is rooted, so that you can use this
+     * wrapper with stack rooting. However, you must not do this if the
+     * JSContext can be destroyed while the Handle is live. */
+    JS::Handle<T>
+    handle(void)
+    {
+        g_assert(m_rooted);
+        return *m_root;
+    }
+
+    /* Roots the GC thing. You must not use this if you're already using the
+     * wrapper to store a non-rooted GC thing. */
+    void
+    root(JSContext    *cx,
+         const T&      thing,
+         DestroyNotify notify = nullptr,
+         void         *data   = nullptr)
+    {
+        debug("root()");
+        g_assert(!m_rooted);
+        g_assert(m_heap.get() == js::GCMethods<T>::initial());
+        m_rooted = true;
+        m_cx = cx;
+        m_notify = notify;
+        m_data = data;
+        m_root = new JS::PersistentRooted<T>(m_cx, thing);
+
+        auto gjs_cx = static_cast<GjsContext *>(JS_GetContextPrivate(m_cx));
+        g_assert(GJS_IS_CONTEXT(gjs_cx));
+        g_object_weak_ref(G_OBJECT(gjs_cx), on_context_destroy, this);
+        m_has_weakref = true;
+    }
+
+    /* You can only assign directly to the GjsMaybeOwned wrapper in the
+     * non-rooted case. */
+    void
+    operator=(const T& thing)
+    {
+        g_assert(!m_rooted);
+        m_heap = thing;
+    }
+
+    void
+    reset(void)
+    {
+        debug("reset()");
+        if (!m_rooted) {
+            m_heap = js::GCMethods<T>::initial();
+            return;
+        }
+
+        teardown_rooting();
+        m_cx = nullptr;
+        m_notify = nullptr;
+        m_data = nullptr;
+    }
+
+    /* Tracing makes no sense in the rooted case, because JS::PersistentRooted
+     * already takes care of that. */
+    void
+    trace(JSTracer   *tracer,
+          const char *name)
+    {
+        debug("trace()");
+        g_assert(!m_rooted);
+        GjsHeapOperation<T>::trace(tracer, &m_heap, name);
+    }
+};
+
+#endif /* GJS_JSAPI_UTIL_ROOT_H */
diff --git a/test/gjs-test-rooting.cpp b/test/gjs-test-rooting.cpp
new file mode 100644
index 0000000..49bdd3d
--- /dev/null
+++ b/test/gjs-test-rooting.cpp
@@ -0,0 +1,237 @@
+#include "gjs/jsapi-util.h"
+#include "gjs/jsapi-util-root.h"
+#include "gjs-test-utils.h"
+
+static GMutex gc_lock;
+static GCond gc_finished;
+static volatile int gc_counter;
+
+#define PARENT(fx) ((GjsUnitTestFixture *)fx)
+typedef struct _GjsRootingFixture GjsRootingFixture;
+struct _GjsRootingFixture {
+    GjsUnitTestFixture parent;
+
+    bool finalized;
+    bool notify_called;
+};
+
+static void
+test_obj_finalize(JSFreeOp *fop,
+                  JSObject *obj)
+{
+    bool *finalized_p = static_cast<bool *>(JS_GetPrivate(obj));
+    g_assert_false(*finalized_p);
+    *finalized_p = true;
+}
+
+static JSClass test_obj_class = {
+    "TestObj",
+    JSCLASS_HAS_PRIVATE,
+    JS_PropertyStub,
+    JS_DeletePropertyStub,
+    JS_PropertyStub,
+    JS_StrictPropertyStub,
+    JS_EnumerateStub,
+    JS_ResolveStub,
+    JS_ConvertStub,
+    test_obj_finalize
+};
+
+static JSObject *
+test_obj_new(GjsRootingFixture *fx)
+{
+    JSObject *retval = JS_NewObject(PARENT(fx)->cx, &test_obj_class,
+                                    JS::NullPtr(), JS::NullPtr());
+    JS_SetPrivate(retval, &fx->finalized);
+    return retval;
+}
+
+static void
+on_gc(JSRuntime *rt,
+      JSGCStatus status,
+      void      *data)
+{
+    if (status != JSGC_END)
+        return;
+
+    g_mutex_lock(&gc_lock);
+    g_atomic_int_inc(&gc_counter);
+    g_cond_broadcast(&gc_finished);
+    g_mutex_unlock(&gc_lock);
+}
+
+static void
+setup(GjsRootingFixture *fx,
+      gconstpointer      unused)
+{
+    gjs_unit_test_fixture_setup(PARENT(fx), unused);
+    JS_SetGCCallback(JS_GetRuntime(PARENT(fx)->cx), on_gc, fx);
+}
+
+static void
+teardown(GjsRootingFixture *fx,
+         gconstpointer      unused)
+{
+    gjs_unit_test_fixture_teardown(PARENT(fx), unused);
+}
+
+static void
+wait_for_gc(GjsRootingFixture *fx)
+{
+    int count = g_atomic_int_get(&gc_counter);
+
+    JS_GC(JS_GetRuntime(PARENT(fx)->cx));
+
+    g_mutex_lock(&gc_lock);
+    while (count == g_atomic_int_get(&gc_counter)) {
+        g_cond_wait(&gc_finished, &gc_lock);
+    }
+    g_mutex_unlock(&gc_lock);
+}
+
+static void
+test_maybe_owned_rooted_keeps_alive_across_gc(GjsRootingFixture *fx,
+                                              gconstpointer      unused)
+{
+    auto obj = new GjsMaybeOwned<JSObject *>();
+    obj->root(PARENT(fx)->cx, test_obj_new(fx));
+
+    wait_for_gc(fx);
+    g_assert_false(fx->finalized);
+
+    delete obj;
+    wait_for_gc(fx);
+    g_assert_true(fx->finalized);
+}
+
+static void
+test_maybe_owned_rooted_is_collected_after_reset(GjsRootingFixture *fx,
+                                                 gconstpointer      unused)
+{
+    auto obj = new GjsMaybeOwned<JSObject *>();
+    obj->root(PARENT(fx)->cx, test_obj_new(fx));
+    obj->reset();
+
+    wait_for_gc(fx);
+    g_assert_true(fx->finalized);
+    delete obj;
+}
+
+static void
+test_maybe_owned_weak_pointer_is_collected_by_gc(GjsRootingFixture *fx,
+                                                 gconstpointer      unused)
+{
+    auto obj = new GjsMaybeOwned<JSObject *>();
+    *obj = test_obj_new(fx);
+
+    wait_for_gc(fx);
+    g_assert_true(fx->finalized);
+    delete obj;
+}
+
+static void
+test_maybe_owned_heap_rooted_keeps_alive_across_gc(GjsRootingFixture *fx,
+                                                   gconstpointer      unused)
+{
+    auto obj = new GjsMaybeOwned<JSObject *>();
+    obj->root(PARENT(fx)->cx, test_obj_new(fx));
+
+    wait_for_gc(fx);
+    g_assert_false(fx->finalized);
+
+    delete obj;
+    wait_for_gc(fx);
+    g_assert_true(fx->finalized);
+}
+
+static void
+context_destroyed(JS::HandleObject obj,
+                  void            *data)
+{
+    auto fx = static_cast<GjsRootingFixture *>(data);
+    g_assert_false(fx->notify_called);
+    g_assert_false(fx->finalized);
+    fx->notify_called = true;
+}
+
+static void
+teardown_context_already_destroyed(GjsRootingFixture *fx,
+                                   gconstpointer      unused)
+{
+    gjs_unit_test_teardown_context_already_destroyed(PARENT(fx));
+}
+
+static void
+test_maybe_owned_notify_callback_called_on_context_destroy(GjsRootingFixture *fx,
+                                                           gconstpointer      unused)
+{
+    auto obj = new GjsMaybeOwned<JSObject *>();
+    obj->root(PARENT(fx)->cx, test_obj_new(fx), context_destroyed, fx);
+
+    gjs_unit_test_destroy_context(PARENT(fx));
+    g_assert_true(fx->notify_called);
+    delete obj;
+}
+
+static void
+test_maybe_owned_object_destroyed_after_notify(GjsRootingFixture *fx,
+                                               gconstpointer      unused)
+{
+    auto obj = new GjsMaybeOwned<JSObject *>();
+    obj->root(PARENT(fx)->cx, test_obj_new(fx), context_destroyed, fx);
+
+    gjs_unit_test_destroy_context(PARENT(fx));
+    g_assert_true(fx->finalized);
+    delete obj;
+}
+
+static void
+delete_obj_callback(JS::HandleObject obj,
+                    void            *data)
+{
+    auto wrapper = static_cast<GjsMaybeOwned<JSObject *> *>(data);
+    delete wrapper;
+}
+
+static void
+test_maybe_owned_can_destroy_object_in_notify_callback(GjsRootingFixture *fx,
+                                                       gconstpointer      unused)
+{
+    auto obj = new GjsMaybeOwned<JSObject *>();
+    obj->root(PARENT(fx)->cx, test_obj_new(fx), delete_obj_callback, obj);
+
+    gjs_unit_test_destroy_context(PARENT(fx));
+    g_assert_true(fx->finalized);
+    /* obj already deleted */
+}
+
+void
+gjs_test_add_tests_for_rooting(void)
+{
+#define ADD_ROOTING_TEST(path, f) \
+    g_test_add("/rooting/" path, GjsRootingFixture, NULL, setup, f,  teardown);
+
+    ADD_ROOTING_TEST("maybe-owned/rooted-keeps-alive-across-gc",
+                     test_maybe_owned_rooted_keeps_alive_across_gc);
+    ADD_ROOTING_TEST("maybe-owned/rooted-is-collected-after-reset",
+                     test_maybe_owned_rooted_is_collected_after_reset);
+    ADD_ROOTING_TEST("maybe-owned/weak-pointer-is-collected-by-gc",
+                     test_maybe_owned_weak_pointer_is_collected_by_gc);
+    ADD_ROOTING_TEST("maybe-owned/heap-rooted-keeps-alive-across-gc",
+                     test_maybe_owned_heap_rooted_keeps_alive_across_gc);
+
+#undef ADD_ROOTING_TEST
+
+#define ADD_CONTEXT_DESTROY_TEST(path, f) \
+    g_test_add("/rooting/" path, GjsRootingFixture, NULL, setup, f, \
+               teardown_context_already_destroyed);
+
+    ADD_CONTEXT_DESTROY_TEST("maybe-owned/notify-callback-called-on-context-destroy",
+                             test_maybe_owned_notify_callback_called_on_context_destroy);
+    ADD_CONTEXT_DESTROY_TEST("maybe-owned/object-destroyed-after-notify",
+                             test_maybe_owned_object_destroyed_after_notify);
+    ADD_CONTEXT_DESTROY_TEST("maybe-owned/can-destroy-object-in-notify-callback",
+                             test_maybe_owned_can_destroy_object_in_notify_callback);
+
+#undef ADD_CONTEXT_DESTROY_TEST
+}
diff --git a/test/gjs-test-utils.cpp b/test/gjs-test-utils.cpp
index 7ed2afd..1540e28 100644
--- a/test/gjs-test-utils.cpp
+++ b/test/gjs-test-utils.cpp
@@ -67,19 +67,30 @@ gjs_unit_test_fixture_setup(GjsUnitTestFixture *fx,
 }
 
 void
-gjs_unit_test_fixture_teardown(GjsUnitTestFixture *fx,
-                               gconstpointer      unused)
+gjs_unit_test_destroy_context(GjsUnitTestFixture *fx)
 {
     JS_LeaveCompartment(fx->cx, fx->compartment);
     JS_EndRequest(fx->cx);
 
     g_object_unref(fx->gjs_context);
+}
 
+void
+gjs_unit_test_teardown_context_already_destroyed(GjsUnitTestFixture *fx)
+{
     if (fx->message != NULL)
         g_printerr("**\n%s\n", fx->message);
     g_free(fx->message);
 }
 
+void
+gjs_unit_test_fixture_teardown(GjsUnitTestFixture *fx,
+                               gconstpointer      unused)
+{
+    gjs_unit_test_destroy_context(fx);
+    gjs_unit_test_teardown_context_already_destroyed(fx);
+}
+
 /* Fork a process that waits the given time then
  * sends us ABRT
  */
diff --git a/test/gjs-test-utils.h b/test/gjs-test-utils.h
index f25483a..12cd9a0 100644
--- a/test/gjs-test-utils.h
+++ b/test/gjs-test-utils.h
@@ -20,6 +20,9 @@
 #ifndef GJS_TEST_UTILS_H
 #define GJS_TEST_UTILS_H
 
+#include "gjs/context.h"
+#include "gjs/jsapi-wrapper.h"
+
 typedef struct _GjsUnitTestFixture GjsUnitTestFixture;
 struct _GjsUnitTestFixture {
     GjsContext *gjs_context;
@@ -31,6 +34,10 @@ struct _GjsUnitTestFixture {
 void gjs_unit_test_fixture_setup(GjsUnitTestFixture *fx,
                                  gconstpointer       unused);
 
+void gjs_unit_test_destroy_context(GjsUnitTestFixture *fx);
+
+void gjs_unit_test_teardown_context_already_destroyed(GjsUnitTestFixture *fx);
+
 void gjs_unit_test_fixture_teardown(GjsUnitTestFixture *fx,
                                     gconstpointer      unused);
 
@@ -40,4 +47,6 @@ void gjs_test_add_tests_for_coverage ();
 
 void gjs_test_add_tests_for_parse_call_args(void);
 
+void gjs_test_add_tests_for_rooting(void);
+
 #endif
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index ca5af1a..a73daf7 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -340,6 +340,7 @@ main(int    argc,
 
     gjs_test_add_tests_for_coverage ();
     gjs_test_add_tests_for_parse_call_args();
+    gjs_test_add_tests_for_rooting();
 
     g_test_run();
 


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