From: Josef 'Jeff' Sipek Date: Wed, 24 Feb 2021 18:57:10 +0000 (-0500) Subject: lib-lua: Add dlua_pcall_yieldable X-Git-Tag: 2.3.15~173 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=1dab5661cfea5422fd1cbc43dd483f63f3e33f28;p=thirdparty%2Fdovecot%2Fcore.git lib-lua: Add dlua_pcall_yieldable --- diff --git a/configure.ac b/configure.ac index 630bc7b534..c27989adaa 100644 --- a/configure.ac +++ b/configure.ac @@ -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]) diff --git a/m4/want_lua.m4 b/m4/want_lua.m4 index 371ff06639..937a149350 100644 --- a/m4/want_lua.m4 +++ b/m4/want_lua.m4 @@ -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" ) diff --git a/src/lib-lua/Makefile.am b/src/lib-lua/Makefile.am index 48c1f9bb69..c2c4e5ac1e 100644 --- a/src/lib-lua/Makefile.am +++ b/src/lib-lua/Makefile.am @@ -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 index 0000000000..763fd193e5 --- /dev/null +++ b/src/lib-lua/dlua-resume.c @@ -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 diff --git a/src/lib-lua/dlua-script-private.h b/src/lib-lua/dlua-script-private.h index 7974cb2aea..bb38ba7b28 100644 --- a/src/lib-lua/dlua-script-private.h +++ b/src/lib-lua/dlua-script-private.h @@ -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);