]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-lua: Add dlua_pcall_yieldable
authorJosef 'Jeff' Sipek <jeff.sipek@open-xchange.com>
Wed, 24 Feb 2021 18:57:10 +0000 (13:57 -0500)
committerAki Tuomi <aki.tuomi@open-xchange.com>
Fri, 19 Mar 2021 13:13:10 +0000 (15:13 +0200)
configure.ac
m4/want_lua.m4
src/lib-lua/Makefile.am
src/lib-lua/dlua-resume.c [new file with mode: 0644]
src/lib-lua/dlua-script-private.h

index 630bc7b5346f57201adb85b5f2937a8798f4c2d6..c27989adaa67c7a93396d4f22edaa7352d79bf25 100644 (file)
@@ -564,6 +564,7 @@ AS_IF([test "x$with_lua" = "xyes"],
       [userdb="$userdb lua (plugin)"; passdb="$passdb lua (plugin)"],
       [userdb="$userdb lua"; passdb="$passdb lua"],
   ), [])
+AM_CONDITIONAL([DLUA_WITH_YIELDS], [test "$dlua_with_yields" = "yes"])
 
 if test $have_modules = yes; then
   AC_DEFINE(HAVE_MODULES,, [Define if you have dynamic module support])
index 371ff066392624505970e4e6b953498b0c5fd24c..937a1493505053e4f71cbdbd0a839f3f2d37e51a 100644 (file)
@@ -52,6 +52,13 @@ AC_DEFUN([DOVECOT_WANT_LUA],[
    AC_CHECK_FUNCS([lua_tointegerx])
    AC_CHECK_FUNCS([lua_yieldk])
 
+   AS_IF([test "$ac_cv_func_lua_resume" = "yes" -a \
+               "$ac_cv_func_lua_yieldk" = "yes"],
+     AC_DEFINE([DLUA_WITH_YIELDS],,
+       [Lua scripts will be able to yield])
+     dlua_with_yields=yes
+   )
+
    CFLAGS="$old_CFLAGS"
    LIBS="$old_LIBS"
   )
index 48c1f9bb69f552fc44492176b7efa205c25bc94e..c2c4e5ac1e4bd4fb8144a490bb7c60e4c1aaa836 100644 (file)
@@ -8,6 +8,7 @@ libdovecot_lua_la_SOURCES = \
        dlua-script.c \
        dlua-dovecot.c \
        dlua-compat.c \
+       dlua-resume.c \
        dlua-table.c \
        dlua-thread.c
 # Note: the only things this lib should depend on are libdovecot and lua.
diff --git a/src/lib-lua/dlua-resume.c b/src/lib-lua/dlua-resume.c
new file mode 100644 (file)
index 0000000..763fd19
--- /dev/null
@@ -0,0 +1,206 @@
+/* Copyright (c) 2021 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "dlua-script-private.h"
+
+#define PCALL_RESUME_STATE "pcall-resume-state"
+
+#define RESUME_TIMEOUT "resume-timeout"
+#define RESUME_NARGS "resume-nargs"
+
+struct dlua_pcall_resume_state {
+       dlua_pcall_yieldable_callback_t *callback;
+       void *context;
+       struct timeout *to;
+       int status;
+};
+
+#ifdef DLUA_WITH_YIELDS
+static void call_resume_callback(lua_State *L)
+{
+       struct dlua_pcall_resume_state *state = dlua_tls_get_ptr(L, PCALL_RESUME_STATE);
+
+       timeout_remove(&state->to);
+
+       dlua_tls_clear(L, PCALL_RESUME_STATE);
+
+       state->callback(L, state->context, state->status);
+
+       i_free(state);
+}
+
+static void queue_resume_callback(lua_State *L, int status)
+{
+       struct dlua_pcall_resume_state *state = dlua_tls_get_ptr(L, PCALL_RESUME_STATE);
+
+       i_assert(status != LUA_YIELD);
+
+       if (status != LUA_OK) {
+               int ret;
+
+               /* error occured: run debug.traceback() */
+
+               /* stack: ..., error (top) */
+               lua_getglobal(L, "debug");
+
+               /* stack: ..., error, debug table (top) */
+               lua_getfield(L, -1, "traceback");
+
+               /* stack: ..., error, debug table, traceback function (top) */
+               lua_remove(L, -2);
+
+               /* stack: ..., error, traceback function (top) */
+               lua_pushvalue(L, -2); /* duplicate original error */
+
+               /* stack: ..., error, traceback function, error (top) */
+
+               /*
+                * Note that we kept the original error on the stack as well
+                * as passed it to debug.traceback().  The reason for that
+                * is that debug.traceback() itself can fail.  If it fails,
+                * it'll generate its own error - which, ultimately, we
+                * don't care about.  For example, consider the following
+                * function:
+                *
+                * function foo()
+                *   debug.traceback = nil
+                *   error("abc")
+                * end
+                *
+                * If we executed this function, it would error out - but
+                * it'd also cause our pcall to debug.traceback() to fail
+                * with "attempt to call a nil value".  We want to discard
+                * the nil error, and just use the original ("abc").  This
+                * is ok because debug.traceback() simply "improves" the
+                * passed in error message to include a traceback and no
+                * traceback is better than a very mysterious error message.
+                */
+               ret = lua_pcall(L, 1, 1, 0);
+
+               /* stack: ..., orig error, traceback result/error (top) */
+
+               if (ret != LUA_OK) {
+                       /* traceback failed, remove its error */
+                       lua_remove(L, -1);
+               } else {
+                       /* traceback succeeded, remove original error */
+                       lua_remove(L, -2);
+               }
+       }
+
+       /*
+        * Mangle the passed in status to match dlua_pcall().  Namely, turn
+        * it into -1 on error, and 0+ to indicate the number of return
+        * values.
+        */
+       if (status == LUA_OK)
+               state->status = lua_gettop(L);
+       else
+               state->status = -1;
+
+       i_assert(state->to == NULL);
+       state->to = timeout_add_short(0, call_resume_callback, L);
+}
+
+static void dlua_pcall_yieldable_continue(lua_State *L)
+{
+       struct timeout *to;
+       int nargs;
+       int ret;
+
+       nargs = dlua_tls_get_int(L, RESUME_NARGS);
+       to = dlua_tls_get_ptr(L, RESUME_TIMEOUT);
+
+       timeout_remove(&to);
+
+       dlua_tls_clear(L, RESUME_TIMEOUT);
+       dlua_tls_clear(L, RESUME_NARGS);
+
+       ret = lua_resume(L, L, nargs);
+       if (ret == LUA_YIELD) {
+               /*
+                * thread yielded - nothing to do
+                *
+                * We assume something will call lua_resume().  We don't
+                * care if it is a io related callback or just a timeout.
+                */
+       } else if (ret == LUA_OK) {
+               /* thread completed - invoke callback */
+               queue_resume_callback(L, ret);
+       } else {
+               /* error occurred - invoke callback */
+               queue_resume_callback(L, ret);
+       }
+}
+
+void dlua_pcall_yieldable_resume(lua_State *L, int nargs)
+{
+       struct timeout *to;
+
+       to = timeout_add_short(0, dlua_pcall_yieldable_continue, L);
+
+       dlua_tls_set_ptr(L, RESUME_TIMEOUT, to);
+       dlua_tls_set_int(L, RESUME_NARGS, nargs);
+}
+
+/*
+ * Call a function with nargs arguments in a way that supports yielding.
+ * When the function execution completes, the passed in callback is called.
+ *
+ * Returns -1 on error or 0 on success.
+ */
+int dlua_pcall_yieldable(lua_State *L, const char *func_name, int nargs,
+                        dlua_pcall_yieldable_callback_t *callback,
+                        void *context, const char **error_r)
+{
+       struct dlua_pcall_resume_state *state;
+       int ret;
+
+       i_assert(lua_status(L) == LUA_OK);
+
+       lua_getglobal(L, func_name);
+
+       if (!lua_isfunction(L, -1)) {
+               /* clean up the stack - function + arguments */
+               lua_pop(L, nargs + 1);
+               *error_r = t_strdup_printf("'%s' is not a function", func_name);
+               return -1;
+       }
+
+       /* allocate and stash in TLS callback state */
+       state = i_new(struct dlua_pcall_resume_state, 1);
+       state->callback = callback;
+       state->context = context;
+
+       dlua_tls_set_ptr(L, PCALL_RESUME_STATE, state);
+
+       /* stack: args, func (top) */
+       lua_insert(L, -(nargs + 1));
+
+       /* stack: func, args (top) */
+       ret = lua_resume(L, L, nargs);
+       if (ret == LUA_YIELD) {
+               /*
+                * thread yielded - nothing to do
+                *
+                * We assume something will call lua_resume().  We don't
+                * care if it is a io related callback or just a timeout.
+                */
+       } else {
+               /*
+                * thread completed / errored
+                *
+                * Since there is nothing that will come back to this lua
+                * thread, we need to make sure the callback is called.
+                *
+                * We handle errors the same as successful completion in
+                * order to avoid forcing the callers to check for lua
+                * errors in two places - the call here and in the callback.
+                */
+               queue_resume_callback(L, ret);
+       }
+
+       return 0;
+}
+#endif
index 7974cb2aea933bd5b55b3d8caab1b9f3e9c759ae..bb38ba7b28d2665d5b74b54e73676d36e0c3803d 100644 (file)
@@ -69,6 +69,8 @@ struct dlua_table_values {
        } v;
 };
 
+typedef void dlua_pcall_yieldable_callback_t(lua_State *L, void *context, int status);
+
 extern struct event_category event_category_lua;
 
 /* assorted wrappers for lua_foo(), but operating on a struct dlua_script */
@@ -175,6 +177,44 @@ lua_State *dlua_script_new_thread(struct dlua_script *script);
 /* Close thread. */
 void dlua_script_close_thread(struct dlua_script *script, lua_State **_L);
 
+#ifdef DLUA_WITH_YIELDS
+/*
+ * Call a function with nargs in a way that supports yielding.
+ *
+ * When the specified function returns, the callback will be called with the
+ * supplied context pointer and a status integer indicating whether an error
+ * occurred (-1) or whether execution completed successfully (0+).  In the
+ * case of a successful completion, the status will indicate the number of
+ * results returned by the function.  On failure, the top of the stack
+ * contains the error object.
+ *
+ * Returns:
+ *  -1 = if function name refers to a non-function type
+ *   0 = function called, callback will be called in the future
+ */
+int dlua_pcall_yieldable(lua_State *L, const char *func_name, int nargs,
+                        void (*callback)(lua_State *, void *, int),
+                        void *context, const char **error_r);
+
+/*
+ * Resume yielded function execution.
+ *
+ * The nargs argument indicates how many items from the top of the stack
+ * should be "returned" by the yield.
+ *
+ * This function is to be called from other API callbacks to resume
+ * execution of the Lua script.  For example, if a Lua script invokes a
+ * function to perform I/O, the function would start the async I/O and yield
+ * from the script.  Eventually, the I/O completion callback executes, which
+ * would call dlua_pcall_yieldable_resume() to continue executing the Lua
+ * script with the supplied arguments.
+ *
+ * Note: The actual execution doesn't resume immediately.  Rather, it is
+ * scheduled to start in the near future via a timeout.
+ */
+void dlua_pcall_yieldable_resume(lua_State *L, int nargs);
+#endif
+
 /* initialize/free script's thread table */
 void dlua_init_thread_table(struct dlua_script *script);
 void dlua_free_thread_table(struct dlua_script *script);