From c5478e9d2bb870cee7be39121952f01f04c5f2b7 Mon Sep 17 00:00:00 2001 From: Josef 'Jeff' Sipek Date: Wed, 24 Feb 2021 14:53:59 -0500 Subject: [PATCH] lib-lua: Add thread local storage API --- src/lib-lua/dlua-script-private.h | 9 ++ src/lib-lua/dlua-thread.c | 165 ++++++++++++++++++++++++++++-- src/lib-lua/test-lua.c | 52 ++++++++++ 3 files changed, 216 insertions(+), 10 deletions(-) diff --git a/src/lib-lua/dlua-script-private.h b/src/lib-lua/dlua-script-private.h index ef378caec4..7974cb2aea 100644 --- a/src/lib-lua/dlua-script-private.h +++ b/src/lib-lua/dlua-script-private.h @@ -179,4 +179,13 @@ void dlua_script_close_thread(struct dlua_script *script, lua_State **_L); void dlua_init_thread_table(struct dlua_script *script); void dlua_free_thread_table(struct dlua_script *script); +/* thread local storage (TLS) getters & setters */ +void dlua_tls_set_ptr(lua_State *L, const char *name, void *ptr); +void *dlua_tls_get_ptr(lua_State *L, const char *name); +void dlua_tls_set_int(lua_State *L, const char *name, lua_Integer i); +lua_Integer dlua_tls_get_int(lua_State *L, const char *name); + +/* free a thread local storage (TLS) value */ +void dlua_tls_clear(lua_State *L, const char *name); + #endif diff --git a/src/lib-lua/dlua-thread.c b/src/lib-lua/dlua-thread.c index 84ac488a1a..875464ca1b 100644 --- a/src/lib-lua/dlua-thread.c +++ b/src/lib-lua/dlua-thread.c @@ -4,15 +4,20 @@ #include "dlua-script-private.h" /* - * dlua support for threads + * dlua support for threads & thread local storage (TLS) * * The following code keeps a table in (global) registry. This table is - * indexed by the thread objects, each mapping to another table. That is: + * indexed by the thread objects, each mapping to another table used to hold + * thread local storage. That is: * * registry[thread] = {} -- threads table + * registry[thread]["foo"] = ... -- TLS value for "foo" * - * This acts as a reference to the thread object, preventing it from being - * garbage collected. + * This serves two purposes: + * + * (1) It provides TLS. + * (2) It acts as a reference to the thread object, preventing it from + * being garbage collected. * * The table is allocated during struct dlua_script's creation and is freed * during the scripts destruction. Any lua threads created using @@ -22,6 +27,9 @@ /* the registry entry with a table with all the lua threads */ #define LUA_THREAD_REGISTRY_KEY "DLUA_THREADS" +static void warn_about_tls_leaks(struct dlua_script *script, lua_State *L); +static void get_tls_table(lua_State *L); + void dlua_init_thread_table(struct dlua_script *script) { lua_newtable(script->L); @@ -29,7 +37,7 @@ void dlua_init_thread_table(struct dlua_script *script) /* * Note that we are *not* adding the main lua state since it is not - * a thread. + * a thread. This implies that it will not have any TLS. */ } @@ -57,6 +65,8 @@ static void warn_about_leaked_threads(struct dlua_script *script) if (lua_type(L, -1) != LUA_TTABLE) { e_error(script->event, "Unexpected %s value in thread table", lua_typename(L, lua_type(L, -1))); + } else { + warn_about_tls_leaks(script, L); } /* pop the value for lua_next() */ @@ -87,22 +97,82 @@ lua_State *dlua_script_new_thread(struct dlua_script *script) thread = lua_newthread(script->L); i_assert(thread != NULL); - /* allocate new per-thread table */ + /* allocate new TLS table */ lua_newtable(script->L); - /* stack: threads-table, thread, per-thread-table (top) */ + /* stack: threads-table, thread, TLS-table (top) */ - /* threads-table[thread] = per-thread-table */ + /* threads-table[thread] = TLS-table */ lua_settable(script->L, -3); return thread; } +static void log_tls_leak(struct dlua_script *script, lua_State *L, bool full) +{ + const char *name = NULL; + + /* stack: TLS key, TLS value (top) */ + + if (full) { + lua_getmetatable(L, -1); + + if (dlua_table_get_string_by_str(L, -1, "__name", &name) < 0) + name = NULL; + + lua_pop(L, 1); /* pop the metatable */ + } + + e_error(script->event, "Lua TLS data in %p thread leaked: key '%s', " + "value %s %p (%s)", L, lua_tostring(L, -2), + full ? "userdata" : "lightuserdata", + lua_touserdata(L, -1), (name != NULL) ? name : ""); +} + +static void warn_about_tls_leaks(struct dlua_script *script, lua_State *L) +{ + i_assert(lua_type(L, -1) == LUA_TTABLE); + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + /* stack: table, key, value (top) */ + + switch (lua_type(L, -1)) { + case LUA_TNIL: + case LUA_TNUMBER: + case LUA_TBOOLEAN: + case LUA_TSTRING: + case LUA_TFUNCTION: + case LUA_TTHREAD: + /* these are trivially freed by the Lua GC */ + break; + case LUA_TTABLE: + /* recurse into the table */ + warn_about_tls_leaks(script, L); + break; + case LUA_TUSERDATA: + log_tls_leak(script, L, TRUE); + break; + case LUA_TLIGHTUSERDATA: + log_tls_leak(script, L, FALSE); + break; + } + + /* pop the value for lua_next() */ + lua_pop(L, 1); + } +} + void dlua_script_close_thread(struct dlua_script *script, lua_State **_L) { if (*_L == NULL) return; + /* log any TLS userdata leaks */ + get_tls_table(*_L); + warn_about_tls_leaks(script, *_L); + lua_pop(*_L, 1); + /* get the threads table */ lua_getfield(*_L, LUA_REGISTRYINDEX, LUA_THREAD_REGISTRY_KEY); @@ -116,10 +186,85 @@ void dlua_script_close_thread(struct dlua_script *script, lua_State **_L) /* * threads-table[thread] = nil * - * This assignment removes the reference to the thread saving it - * from GC. + * This assignment (1) frees all TLS for the thread, and (2) removes + * the reference to the thread saving it from GC. */ lua_settable(*_L, -3); *_L = NULL; } + +/* get the current thread's TLS table */ +static void get_tls_table(lua_State *L) +{ + int ret; + + /* get the threads table */ + ret = dlua_table_get_by_str(L, LUA_REGISTRYINDEX, LUA_TTABLE, + LUA_THREAD_REGISTRY_KEY); + if (ret < 1) + luaL_error(L, "lua threads table is %s", + (ret == 0) ? "missing" : "not a table"); + + /* get the TLS-table */ + ret = dlua_table_get_by_thread(L, -1, LUA_TTABLE); + if (ret < 1) + luaL_error(L, "lua TLS table for thread %p is not a table", L); + + /* stack: threads-table, TLS-table (top) */ + + /* remove threads-table from stack */ + lua_remove(L, -2); +} + +void dlua_tls_set_ptr(lua_State *L, const char *name, void *ptr) +{ + get_tls_table(L); + lua_pushlightuserdata(L, ptr); + lua_setfield(L, -2, name); + lua_pop(L, 1); +} + +void *dlua_tls_get_ptr(lua_State *L, const char *name) +{ + void *ptr; + + get_tls_table(L); + lua_getfield(L, -1, name); + + ptr = lua_touserdata(L, -1); + + lua_pop(L, 2); + + return ptr; +} + +void dlua_tls_set_int(lua_State *L, const char *name, lua_Integer i) +{ + get_tls_table(L); + lua_pushinteger(L, i); + lua_setfield(L, -2, name); + lua_pop(L, 1); +} + +lua_Integer dlua_tls_get_int(lua_State *L, const char *name) +{ + lua_Integer i; + + get_tls_table(L); + lua_getfield(L, -1, name); + + i = lua_tointeger(L, -1); + + lua_pop(L, 2); + + return i; +} + +void dlua_tls_clear(lua_State *L, const char *name) +{ + get_tls_table(L); + lua_pushnil(L); + lua_setfield(L, -2, name); + lua_pop(L, 1); +} diff --git a/src/lib-lua/test-lua.c b/src/lib-lua/test-lua.c index 375a53c982..cdce7b1e37 100644 --- a/src/lib-lua/test-lua.c +++ b/src/lib-lua/test-lua.c @@ -305,6 +305,57 @@ static void test_lua(void) test_end(); } +static void test_tls(void) +{ + const char *error = NULL; + struct dlua_script *script = NULL; + lua_State *L1, *L2; + + test_begin("lua thread local storage"); + + test_assert(dlua_script_create_string("", &script, NULL, &error) == 0); + if (error != NULL) + i_fatal("dlua_script_init failed: %s", error); + + L1 = dlua_script_new_thread(script); + L2 = dlua_script_new_thread(script); + + dlua_tls_set_ptr(L1, "ptr", L1); + test_assert(dlua_tls_get_ptr(L1, "ptr") == L1); + test_assert(dlua_tls_get_ptr(L2, "ptr") == NULL); + test_assert(dlua_tls_get_int(L1, "int") == 0); + test_assert(dlua_tls_get_int(L2, "int") == 0); + + dlua_tls_set_ptr(L2, "ptr", L2); + test_assert(dlua_tls_get_ptr(L1, "ptr") == L1); + test_assert(dlua_tls_get_ptr(L2, "ptr") == L2); + test_assert(dlua_tls_get_int(L1, "int") == 0); + test_assert(dlua_tls_get_int(L2, "int") == 0); + + dlua_tls_set_int(L1, "int", 1); + dlua_tls_set_int(L2, "int", 2); + test_assert(dlua_tls_get_int(L1, "int") == 1); + test_assert(dlua_tls_get_int(L2, "int") == 2); + + dlua_tls_clear(L1, "ptr"); + test_assert(dlua_tls_get_ptr(L1, "ptr") == NULL); + test_assert(dlua_tls_get_ptr(L2, "ptr") == L2); + test_assert(dlua_tls_get_int(L1, "int") == 1); + test_assert(dlua_tls_get_int(L2, "int") == 2); + + dlua_script_close_thread(script, &L1); + + test_assert(dlua_tls_get_ptr(L2, "ptr") == L2); + test_assert(dlua_tls_get_int(L2, "int") == 2); + + dlua_tls_clear(L2, "ptr"); + dlua_script_close_thread(script, &L2); + + dlua_script_unref(&script); + + test_end(); +} + /* check lua_tointegerx against top-of-stack item */ static void check_tointegerx_compat(lua_State *L, int expected_isnum, int expected_isint, @@ -414,6 +465,7 @@ static void test_compat_tointegerx_and_isinteger(void) int main(void) { void (*tests[])(void) = { test_lua, + test_tls, test_compat_tointegerx_and_isinteger, NULL }; -- 2.47.3