[gjs/ewlsh/main-loop-hooks] Introduce runAsync() to run mainloops without blocking module resolution
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/main-loop-hooks] Introduce runAsync() to run mainloops without blocking module resolution
- Date: Sat, 7 May 2022 07:01:58 +0000 (UTC)
commit a2f90b18cbcd07b9324ddccf08bfcde0d4bd615e
Author: Evan Welsh <contact evanwelsh com>
Date: Sat May 7 00:01:52 2022 -0700
Introduce runAsync() to run mainloops without blocking module resolution
With top-level await all modules are now promises, if Gtk.Application.run()
or GLib.MainLoop.run() is called within a module it will block
all other promises as run() is a synchronous, blocking function.
To work around this there is now a setMainLoopHook function exposed
which runs a callback after module resolution is complete. This allows
APIs to "install" a mainloop asynchronously.
For Gio.Application, Gtk.Application, and GLib.MainLoop there are now
runAsync() versions of their run() functions. runAsync() returns a
Promise which will resolve once the mainloop has exited.
Fixes #468
gjs/context-private.h | 15 +++++++
gjs/context.cpp | 91 ++++++++++++++++++++++++++++++++++++++----
gjs/mainloop.cpp | 2 +
gjs/promise.cpp | 29 +++++++++++++-
modules/core/overrides/GLib.js | 13 ++++++
modules/core/overrides/Gio.js | 2 +
6 files changed, 143 insertions(+), 9 deletions(-)
---
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 3bc7d50fa..83b9c646b 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -65,6 +65,7 @@ class GjsContextPrivate : public JS::JobQueue {
private:
GjsContext* m_public_context;
JSContext* m_cx;
+ JS::Heap<JSFunction*> m_main_loop_hook;
JS::Heap<JSObject*> m_global;
JS::Heap<JSObject*> m_internal_global;
std::thread::id m_owner_thread;
@@ -176,6 +177,20 @@ class GjsContextPrivate : public JS::JobQueue {
[[nodiscard]] GjsContext* public_context() const {
return m_public_context;
}
+ [[nodiscard]] bool set_main_loop_hook(JSFunction* callback) {
+ if (!callback) {
+ m_main_loop_hook = callback;
+ return true;
+ }
+
+ if (m_main_loop_hook)
+ return false;
+
+ m_main_loop_hook = callback;
+ return true;
+ }
+ [[nodiscard]] bool has_main_loop_hook() { return !!m_main_loop_hook; }
+ GJS_JSAPI_RETURN_CONVENTION bool run_main_loop_hook();
[[nodiscard]] JSContext* context() const { return m_cx; }
[[nodiscard]] JSObject* global() const { return m_global.get(); }
[[nodiscard]] JSObject* internal_global() const {
diff --git a/gjs/context.cpp b/gjs/context.cpp
index b1cb934bc..900c3f6a1 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -349,6 +349,8 @@ void GjsContextPrivate::trace(JSTracer* trc, void* data) {
JS::TraceEdge<JSObject*>(trc, &gjs->m_global, "GJS global object");
JS::TraceEdge<JSObject*>(trc, &gjs->m_internal_global,
"GJS internal global object");
+ JS::TraceEdge<JSFunction*>(trc, &gjs->m_main_loop_hook,
+ "GJS main loop hook");
gjs->m_atoms->trace(trc);
gjs->m_job_queue.trace(trc);
gjs->m_object_init_list.trace(trc);
@@ -460,6 +462,7 @@ void GjsContextPrivate::dispose(void) {
JS_RemoveExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);
m_global = nullptr;
m_internal_global = nullptr;
+ m_main_loop_hook = nullptr;
gjs_debug(GJS_DEBUG_CONTEXT, "Freeing allocated resources");
delete m_fundamental_table;
@@ -1354,6 +1357,14 @@ bool GjsContextPrivate::handle_exit_code(bool no_sync_error_pending,
return false;
}
+bool GjsContextPrivate::run_main_loop_hook() {
+ JS::RootedFunction hook(m_cx, m_main_loop_hook.get());
+ m_main_loop_hook = nullptr;
+ JS::RootedValue ignored_rval(m_cx);
+ return JS_CallFunction(m_cx, nullptr, hook, JS::HandleValueArray::empty(),
+ &ignored_rval);
+}
+
bool GjsContextPrivate::eval(const char* script, size_t script_len,
const char* filename, int* exit_status_p,
GError** error) {
@@ -1366,15 +1377,47 @@ bool GjsContextPrivate::eval(const char* script, size_t script_len,
JS::RootedValue retval(m_cx);
bool ok = eval_with_scope(nullptr, script, script_len, filename, &retval);
- /* The promise job queue should be drained even on error, to finish
- * outstanding async tasks before the context is torn down. Drain after
- * uncaught exceptions have been reported since draining runs callbacks.
+ /**
+ * If there are no errors and the mainloop hook
+ * is set, call it.
+ */
+ if (ok && m_main_loop_hook)
+ ok = run_main_loop_hook();
+
+ bool exiting = false;
+
+ /**
+ * Spin the internal loop until the main loop hook
+ * is set or no holds remain.
*
* If the main loop returns false we cannot guarantee the state
* of our promise queue (a module promise could be pending) so
* instead of draining the queue we instead just exit.
*/
- if (!ok || m_main_loop.spin(this)) {
+ if (ok && !m_main_loop.spin(this)) {
+ exiting = true;
+ }
+
+ // If the hook has been set again, enter a loop
+ // until an error is encountered or the main loop
+ // is quit.
+ while (ok && !exiting && m_main_loop_hook) {
+ ok = run_main_loop_hook();
+
+ // Additional jobs could have been enqueued from the
+ // main loop hook
+ if (ok && !m_main_loop.spin(this)) {
+ exiting = true;
+ }
+ }
+
+ /* The promise job queue should be drained even on error, to finish
+ * outstanding async tasks before the context is torn down. Drain after
+ * uncaught exceptions have been reported since draining runs callbacks.
+ *
+ * We do not drain if we are exiting.
+ */
+ if (!ok && !exiting) {
JS::AutoSaveExceptionState saved_exc(m_cx);
ok = run_jobs_fallible() && ok;
}
@@ -1439,15 +1482,47 @@ bool GjsContextPrivate::eval_module(const char* identifier,
on_context_module_rejected_log_exception, "context");
}
- /* The promise job queue should be drained even on error, to finish
- * outstanding async tasks before the context is torn down. Drain after
- * uncaught exceptions have been reported since draining runs callbacks.
+ /**
+ * If there are no errors and the mainloop hook
+ * is set, call it.
+ */
+ if (ok && m_main_loop_hook)
+ ok = run_main_loop_hook();
+
+ bool exiting = false;
+
+ /**
+ * Spin the internal loop until the main loop hook
+ * is set or no holds remain.
*
* If the main loop returns false we cannot guarantee the state
* of our promise queue (a module promise could be pending) so
* instead of draining the queue we instead just exit.
*/
- if (!ok || m_main_loop.spin(this)) {
+ if (ok && !m_main_loop.spin(this)) {
+ exiting = true;
+ }
+
+ // If the hook has been set again, enter a loop
+ // until an error is encountered or the main loop
+ // is quit.
+ while (ok && !exiting && m_main_loop_hook) {
+ ok = run_main_loop_hook();
+
+ // Additional jobs could have been enqueued from the
+ // main loop hook
+ if (ok && !m_main_loop.spin(this)) {
+ exiting = true;
+ }
+ }
+
+ /* The promise job queue should be drained even on error, to finish
+ * outstanding async tasks before the context is torn down. Drain after
+ * uncaught exceptions have been reported since draining runs callbacks.
+ *
+ * We do not drain if we are exiting.
+ */
+ if (!ok && !exiting) {
JS::AutoSaveExceptionState saved_exc(m_cx);
ok = run_jobs_fallible() && ok;
}
diff --git a/gjs/mainloop.cpp b/gjs/mainloop.cpp
index 91109d61a..b69d45a85 100644
--- a/gjs/mainloop.cpp
+++ b/gjs/mainloop.cpp
@@ -40,6 +40,8 @@ bool MainLoop::spin(GjsContextPrivate* gjs) {
return false;
}
} while (
+ // and there is not a pending main loop hook
+ !gjs->has_main_loop_hook() &&
// and there are pending sources or the job queue is not empty
// continue spinning the event loop.
(can_block() || !gjs->empty()));
diff --git a/gjs/promise.cpp b/gjs/promise.cpp
index 0ce4bd7c5..d8521150e 100644
--- a/gjs/promise.cpp
+++ b/gjs/promise.cpp
@@ -14,8 +14,10 @@
#include <js/RootingAPI.h>
#include <js/TypeDecls.h>
#include <jsapi.h> // for JS_DefineFunctions, JS_NewPlainObject
+#include <jsfriendapi.h>
#include "gjs/context-private.h"
+#include "gjs/jsapi-util-args.h"
#include "gjs/jsapi-util.h"
#include "gjs/macros.h"
#include "gjs/promise.h"
@@ -194,8 +196,33 @@ bool drain_microtask_queue(JSContext* cx, unsigned argc, JS::Value* vp) {
return true;
}
+GJS_JSAPI_RETURN_CONVENTION
+bool set_main_loop_hook(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ JS::RootedObject callback(cx);
+ if (!gjs_parse_call_args(cx, "setMainLoopHook", args, "o", "callback",
+ &callback)) {
+ return false;
+ }
+
+ JS::RootedFunction func(cx, JS_GetObjectFunction(callback));
+
+ GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
+ if (!priv->set_main_loop_hook(func)) {
+ gjs_throw(
+ cx,
+ "A mainloop is already running. Did you already call runAsync()?");
+ return false;
+ }
+
+ args.rval().setUndefined();
+ return true;
+}
+
JSFunctionSpec gjs_native_promise_module_funcs[] = {
- JS_FN("drainMicrotaskQueue", &drain_microtask_queue, 0, 0), JS_FS_END};
+ JS_FN("drainMicrotaskQueue", &drain_microtask_queue, 0, 0),
+ JS_FN("setMainLoopHook", &set_main_loop_hook, 1, 0), JS_FS_END};
bool gjs_define_native_promise_stuff(JSContext* cx,
JS::MutableHandleObject module) {
diff --git a/modules/core/overrides/GLib.js b/modules/core/overrides/GLib.js
index cb8f177e7..9083c4ff4 100644
--- a/modules/core/overrides/GLib.js
+++ b/modules/core/overrides/GLib.js
@@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: 2011 Giovanni Campagna
const ByteArray = imports.byteArray;
+const {setMainLoopHook} = imports._promiseNative;
let GLib;
@@ -261,6 +262,18 @@ function _init() {
GLib = this;
+ GLib.MainLoop.prototype.runAsync = function (...args) {
+ return new Promise((resolve, reject) => {
+ setMainLoopHook(() => {
+ try {
+ resolve(this.run(...args));
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+ };
+
// For convenience in property min or max values, since GLib.MAXINT64 and
// friends will log a warning when used
this.MAXINT64_BIGINT = 0x7fff_ffff_ffff_ffffn;
diff --git a/modules/core/overrides/Gio.js b/modules/core/overrides/Gio.js
index be2e52470..e9bd8f541 100644
--- a/modules/core/overrides/Gio.js
+++ b/modules/core/overrides/Gio.js
@@ -442,6 +442,8 @@ function _promisify(proto, asyncFunc,
function _init() {
Gio = this;
+ Gio.Application.prototype.runAsync = GLib.MainLoop.prototype.runAsync;
+
Gio.DBus = {
get session() {
return Gio.bus_get_sync(Gio.BusType.SESSION, null);
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]