From 5c7ef34404d3367542275d76b49f276ab035639f Mon Sep 17 00:00:00 2001
From: Benoit Germain <benoit.germain@ubisoft.com>
Date: Tue, 7 May 2024 09:42:09 +0200
Subject: Some more code refactorization

---
 src/compat.cpp       |   2 +-
 src/compat.h         |   2 +
 src/lanes.cpp        | 135 +++++++--------------------------------------------
 src/lanes_private.h  |  10 +++-
 src/lindafactory.cpp |   5 +-
 src/state.cpp        | 107 ++++++++++++++++++++++------------------
 src/tools.cpp        |   2 +-
 src/universe.cpp     | 116 ++++++++++++++++++++++++++++++++++---------
 src/universe.h       |  36 ++++++++++++++
 9 files changed, 223 insertions(+), 192 deletions(-)

(limited to 'src')

diff --git a/src/compat.cpp b/src/compat.cpp
index 1d38917..336f716 100644
--- a/src/compat.cpp
+++ b/src/compat.cpp
@@ -29,7 +29,7 @@ LuaType luaG_getmodule(lua_State* L_, char const* name_)
 // #################################################################################################
 
 // Copied from Lua 5.2 loadlib.c
-static int luaL_getsubtable(lua_State* L_, int idx_, const char* fname_)
+int luaL_getsubtable(lua_State* L_, int idx_, const char* fname_)
 {
     lua_getfield(L_, idx_, fname_);
     if (lua_istable(L_, -1))
diff --git a/src/compat.h b/src/compat.h
index 3a61268..bf22f10 100644
--- a/src/compat.h
+++ b/src/compat.h
@@ -68,6 +68,8 @@ inline int lua504_dump(lua_State* L_, lua_Writer writer_, void* data_, [[maybe_u
 }
 #define LUA_LOADED_TABLE "_LOADED" // // doesn't exist in Lua 5.1
 
+int luaL_getsubtable(lua_State* L_, int idx_, const char* fname_);
+
 #endif // LUA_VERSION_NUM == 501
 
 // #################################################################################################
diff --git a/src/lanes.cpp b/src/lanes.cpp
index bce75a6..90f0f9f 100644
--- a/src/lanes.cpp
+++ b/src/lanes.cpp
@@ -393,14 +393,6 @@ static void push_stack_trace(lua_State* L_, int rc_, int stk_base_)
 // ########################################### Threads #############################################
 // #################################################################################################
 
-//
-// Protects modifying the selfdestruct chain
-
-#define SELFDESTRUCT_END ((Lane*) (-1))
-//
-// The chain is ended by '(Lane*)(-1)', not nullptr:
-//      'selfdestructFirst -> ... -> ... -> (-1)'
-
 /*
  * Add the lane to selfdestruct chain; the ones still running at the end of the
  * whole process will be cancelled.
@@ -446,99 +438,6 @@ static void selfdestruct_add(Lane* lane_)
 
 // #################################################################################################
 
-// process end: cancel any still free-running threads
-[[nodiscard]] static int universe_gc(lua_State* L_)
-{
-    Universe* const U{ lua_tofulluserdata<Universe>(L_, 1) };
-    lua_Duration const shutdown_timeout{ lua_tonumber(L_, lua_upvalueindex(1)) };
-    [[maybe_unused]] char const* const op_string{ lua_tostring(L_, lua_upvalueindex(2)) };
-    CancelOp const op{ which_cancel_op(op_string) };
-
-    if (U->selfdestructFirst != SELFDESTRUCT_END) {
-        // Signal _all_ still running threads to exit (including the timer thread)
-        {
-            std::lock_guard<std::mutex> guard{ U->selfdestructMutex };
-            Lane* lane{ U->selfdestructFirst };
-            lua_Duration timeout{ 1us };
-            while (lane != SELFDESTRUCT_END) {
-                // attempt the requested cancel with a small timeout.
-                // if waiting on a linda, they will raise a cancel_error.
-                // if a cancellation hook is desired, it will be installed to try to raise an error
-                if (lane->thread.joinable()) {
-                    std::ignore = thread_cancel(lane, op, 1, timeout, true);
-                }
-                lane = lane->selfdestruct_next;
-            }
-        }
-
-        // When noticing their cancel, the lanes will remove themselves from the selfdestruct chain.
-        {
-            std::chrono::time_point<std::chrono::steady_clock> t_until{ std::chrono::steady_clock::now() + std::chrono::duration_cast<std::chrono::steady_clock::duration>(shutdown_timeout) };
-
-            while (U->selfdestructFirst != SELFDESTRUCT_END) {
-                // give threads time to act on their cancel
-                std::this_thread::yield();
-                // count the number of cancelled thread that didn't have the time to act yet
-                int n{ 0 };
-                {
-                    std::lock_guard<std::mutex> guard{ U->selfdestructMutex };
-                    Lane* lane{ U->selfdestructFirst };
-                    while (lane != SELFDESTRUCT_END) {
-                        if (lane->cancelRequest != CancelRequest::None)
-                            ++n;
-                        lane = lane->selfdestruct_next;
-                    }
-                }
-                // if timeout elapsed, or we know all threads have acted, stop waiting
-                std::chrono::time_point<std::chrono::steady_clock> t_now = std::chrono::steady_clock::now();
-                if (n == 0 || (t_now >= t_until)) {
-                    DEBUGSPEW_CODE(fprintf(stderr, "%d uncancelled lane(s) remain after waiting %fs at process end.\n", n, shutdown_timeout.count()));
-                    break;
-                }
-            }
-        }
-
-        // If some lanes are currently cleaning after themselves, wait until they are done.
-        // They are no longer listed in the selfdestruct chain, but they still have to lua_close().
-        while (U->selfdestructingCount.load(std::memory_order_acquire) > 0) {
-            std::this_thread::yield();
-        }
-    }
-
-    // If after all this, we still have some free-running lanes, it's an external user error, they should have stopped appropriately
-    {
-        std::lock_guard<std::mutex> guard{ U->selfdestructMutex };
-        Lane* lane{ U->selfdestructFirst };
-        if (lane != SELFDESTRUCT_END) {
-            // this causes a leak because we don't call U's destructor (which could be bad if the still running lanes are accessing it)
-            raise_luaL_error(L_, "Zombie thread %s refuses to die!", lane->debugName);
-        }
-    }
-
-    // no need to mutex-protect this as all threads in the universe are gone at that point
-    if (U->timerLinda != nullptr) { // test in case some early internal error prevented Lanes from creating the deep timer
-        [[maybe_unused]] int const prev_ref_count{ U->timerLinda->refcount.fetch_sub(1, std::memory_order_relaxed) };
-        LUA_ASSERT(L_, prev_ref_count == 1); // this should be the last reference
-        DeepFactory::DeleteDeepObject(L_, U->timerLinda);
-        U->timerLinda = nullptr;
-    }
-
-    close_keepers(U);
-
-    // remove the protected allocator, if any
-    U->protectedAllocator.removeFrom(L_);
-
-    U->Universe::~Universe();
-
-    // universe is no longer available (nor necessary)
-    // we need to do this in case some deep userdata objects were created before Lanes was initialized,
-    // as potentially they will be garbage collected after Lanes at application shutdown
-    universe_store(L_, nullptr);
-    return 0;
-}
-
-// #################################################################################################
-
 //---
 // = _single( [cores_uint=1] )
 //
@@ -1526,14 +1425,13 @@ LUAG_FUNC(threads)
     Universe* const U{ universe_get(L_) };
 
     // List _all_ still running threads
-    //
     std::lock_guard<std::mutex> guard{ U->trackingMutex };
     if (U->trackingFirst && U->trackingFirst != TRACKING_END) {
         Lane* lane{ U->trackingFirst };
         int index{ 0 };
         lua_newtable(L_);                                                                          // L_: {}
         while (lane != TRACKING_END) {
-            // insert a { name, status } tuple, so that several lanes with the same name can't clobber each other
+            // insert a { name='<name>', status='<status>' } tuple, so that several lanes with the same name can't clobber each other
             lua_createtable(L_, 0, 2);                                                             // L_: {} {}
             lua_pushstring(L_, lane->debugName);                                                   // L_: {} {} "name"
             lua_setfield(L_, -2, "name");                                                          // L_: {} {}
@@ -1622,17 +1520,20 @@ LUAG_FUNC(wakeup_conv)
 // #################################################################################################
 
 extern int LG_linda(lua_State* L_);
-static struct luaL_Reg const lanes_functions[] = {
-    { "linda", LG_linda },
-    { "now_secs", LG_now_secs },
-    { "wakeup_conv", LG_wakeup_conv },
-    { "set_thread_priority", LG_set_thread_priority },
-    { "set_thread_affinity", LG_set_thread_affinity },
-    { "nameof", luaG_nameof },
-    { "register", LG_register },
-    { "set_singlethreaded", LG_set_singlethreaded },
-    { nullptr, nullptr }
-};
+
+namespace global {
+    static struct luaL_Reg const sLanesFunctions[] = {
+        { "linda", LG_linda },
+        { "now_secs", LG_now_secs },
+        { "wakeup_conv", LG_wakeup_conv },
+        { "set_thread_priority", LG_set_thread_priority },
+        { "set_thread_affinity", LG_set_thread_affinity },
+        { "nameof", luaG_nameof },
+        { "register", LG_register },
+        { "set_singlethreaded", LG_set_singlethreaded },
+        { nullptr, nullptr }
+    };
+} // namespace global
 
 // #################################################################################################
 
@@ -1715,7 +1616,7 @@ LUAG_FUNC(configure)
     lua_pushnil(L_);                                                                               // L_: settings M nil
     lua_setfield(L_, -2, "configure");                                                             // L_: settings M
     // add functions to the module's table
-    luaG_registerlibfuncs(L_, lanes_functions);
+    luaG_registerlibfuncs(L_, global::sLanesFunctions);
 #if HAVE_LANE_TRACKING()
     // register core.threads() only if settings say it should be available
     if (U->trackingFirst != nullptr) {
@@ -1739,7 +1640,7 @@ LUAG_FUNC(configure)
     // prepare the metatable for threads
     // contains keys: { __gc, __index, cached_error, cached_tostring, cancel, join, get_debug_threadname }
     //
-    if (luaL_newmetatable(L_, "Lane")) {                                                           // L_: settings M mt
+    if (luaL_newmetatable(L_, kLaneMetatableName)) {                                               // L_: settings M mt
         lua_pushcfunction(L_, lane_gc);                                                            // L_: settings M mt lane_gc
         lua_setfield(L_, -2, "__gc");                                                              // L_: settings M mt
         lua_pushcfunction(L_, LG_thread_index);                                                    // L_: settings M mt LG_thread_index
@@ -1756,7 +1657,7 @@ LUAG_FUNC(configure)
         lua_setfield(L_, -2, "get_debug_threadname");                                              // L_: settings M mt
         lua_pushcfunction(L_, LG_thread_cancel);                                                   // L_: settings M mt LG_thread_cancel
         lua_setfield(L_, -2, "cancel");                                                            // L_: settings M mt
-        lua_pushliteral(L_, "Lane");                                                               // L_: settings M mt "Lane"
+        lua_pushliteral(L_, kLaneMetatableName);                                                   // L_: settings M mt "Lane"
         lua_setfield(L_, -2, "__metatable");                                                       // L_: settings M mt
     }
 
diff --git a/src/lanes_private.h b/src/lanes_private.h
index 309b632..196a346 100644
--- a/src/lanes_private.h
+++ b/src/lanes_private.h
@@ -10,6 +10,14 @@
 #include <stop_token>
 #include <thread>
 
+// The chain is ended by '(Lane*)(-1)', not nullptr: 'selfdestructFirst -> ... -> ... -> (-1)'
+#define SELFDESTRUCT_END ((Lane*) (-1))
+
+// must be a #define instead of a constexpr to work with lua_pushliteral (until I templatize it)
+#define kLaneMetatableName "Lane"
+#define kLanesLibName "lanes"
+#define kLanesCoreLibName kLanesLibName ".core"
+
 // NOTE: values to be changed by either thread, during execution, without
 //       locking, are marked "volatile"
 //
@@ -102,5 +110,5 @@ static constexpr RegistryUniqueKey kLanePointerRegKey{ 0x2D8CF03FE9F0A51Aull };
 //
 [[nodiscard]] inline Lane* ToLane(lua_State* L_, int i_)
 {
-    return *(static_cast<Lane**>(luaL_checkudata(L_, i_, "Lane")));
+    return *(static_cast<Lane**>(luaL_checkudata(L_, i_, kLaneMetatableName)));
 }
diff --git a/src/lindafactory.cpp b/src/lindafactory.cpp
index 1a8782e..0ec5a0a 100644
--- a/src/lindafactory.cpp
+++ b/src/lindafactory.cpp
@@ -34,6 +34,9 @@ THE SOFTWARE.
 
 #include "linda.h"
 
+// must be a #define instead of a constexpr to work with lua_pushliteral (until I templatize it)
+#define kLindaMetatableName "Linda"
+
 // #################################################################################################
 
 void LindaFactory::createMetatable(lua_State* L_) const
@@ -45,7 +48,7 @@ void LindaFactory::createMetatable(lua_State* L_) const
     lua_setfield(L_, -2, "__index");
 
     // protect metatable from external access
-    lua_pushliteral(L_, "Linda");
+    lua_pushliteral(L_, kLindaMetatableName);
     lua_setfield(L_, -2, "__metatable");
 
     // the linda functions
diff --git a/src/state.cpp b/src/state.cpp
index a3dfbcd..f894978 100644
--- a/src/state.cpp
+++ b/src/state.cpp
@@ -34,6 +34,7 @@ THE SOFTWARE.
 #include "state.h"
 
 #include "lanes.h"
+#include "lanes_private.h"
 #include "tools.h"
 #include "universe.h"
 
@@ -111,68 +112,71 @@ void serialize_require(DEBUGSPEW_PARAM_COMMA(Universe* U_) lua_State* L_)
 [[nodiscard]] static int require_lanes_core(lua_State* L_)
 {
     // leaves a copy of 'lanes.core' module table on the stack
-    luaL_requiref(L_, "lanes.core", luaopen_lanes_core, 0);
+    luaL_requiref(L_, kLanesCoreLibName, luaopen_lanes_core, 0);
     return 1;
 }
 
 // #################################################################################################
 
-static luaL_Reg const libs[] = {
-    { LUA_LOADLIBNAME, luaopen_package },
-    { LUA_TABLIBNAME, luaopen_table },
-    { LUA_STRLIBNAME, luaopen_string },
-    { LUA_MATHLIBNAME, luaopen_math },
+namespace global
+{
+    static luaL_Reg const sLibs[] = {
+        { "base", nullptr }, // ignore "base" (already acquired it)
+#if LUA_VERSION_NUM >= 502
+#ifdef luaopen_bit32
+        { LUA_BITLIBNAME, luaopen_bit32 },
+#endif
+        { LUA_COLIBNAME, luaopen_coroutine }, // Lua 5.2: coroutine is no longer a part of base!
+#else // LUA_VERSION_NUM
+        { LUA_COLIBNAME, nullptr }, // Lua 5.1: part of base package
+#endif // LUA_VERSION_NUM
+        { LUA_DBLIBNAME, luaopen_debug },
 #ifndef PLATFORM_XBOX // no os/io libs on xbox
-    { LUA_OSLIBNAME, luaopen_os },
-    { LUA_IOLIBNAME, luaopen_io },
+        { LUA_IOLIBNAME, luaopen_io },
+        { LUA_OSLIBNAME, luaopen_os },
 #endif // PLATFORM_XBOX
+        { LUA_LOADLIBNAME, luaopen_package },
+        { LUA_MATHLIBNAME, luaopen_math },
+        { LUA_STRLIBNAME, luaopen_string },
+        { LUA_TABLIBNAME, luaopen_table },
 #if LUA_VERSION_NUM >= 503
-    { LUA_UTF8LIBNAME, luaopen_utf8 },
+        { LUA_UTF8LIBNAME, luaopen_utf8 },
 #endif
-#if LUA_VERSION_NUM >= 502
-#ifdef luaopen_bit32
-    { LUA_BITLIBNAME, luaopen_bit32 },
-#endif
-    { LUA_COLIBNAME, luaopen_coroutine }, // Lua 5.2: coroutine is no longer a part of base!
-#else                                     // LUA_VERSION_NUM
-    { LUA_COLIBNAME, nullptr },          // Lua 5.1: part of base package
-#endif                                    // LUA_VERSION_NUM
-    { LUA_DBLIBNAME, luaopen_debug },
 #if LUAJIT_FLAVOR() != 0 // building against LuaJIT headers, add some LuaJIT-specific libs
-                         // #pragma message( "supporting JIT base libs")
-    { LUA_BITLIBNAME, luaopen_bit },
-    { LUA_JITLIBNAME, luaopen_jit },
-    { LUA_FFILIBNAME, luaopen_ffi },
+        { LUA_BITLIBNAME, luaopen_bit },
+        { LUA_FFILIBNAME, luaopen_ffi },
+        { LUA_JITLIBNAME, luaopen_jit },
 #endif // LUAJIT_FLAVOR()
 
-    { LUA_DBLIBNAME, luaopen_debug },
-    { "lanes.core", require_lanes_core }, // So that we can open it like any base library (possible since we have access to the init function)
-                                          //
-    { "base", nullptr },                  // ignore "base" (already acquired it)
-    { nullptr, nullptr }
-};
+        { kLanesCoreLibName, require_lanes_core }, // So that we can open it like any base library (possible since we have access to the init function)
+                                                   //
+        { nullptr, nullptr }
+    };
+
+} // namespace global
 
 // #################################################################################################
 
 static void open1lib(DEBUGSPEW_PARAM_COMMA(Universe* U_) lua_State* L_, char const* name_, size_t len_)
 {
-    for (int i{ 0 }; libs[i].name; ++i) {
-        if (strncmp(name_, libs[i].name, len_) == 0) {
-            lua_CFunction libfunc = libs[i].func;
-            name_ = libs[i].name; // note that the provided name_ doesn't necessarily ends with '\0', hence len_
-            if (libfunc != nullptr) {
-                bool const isLanesCore{ libfunc == require_lanes_core }; // don't want to create a global for "lanes.core"
-                DEBUGSPEW_CODE(fprintf(stderr, INDENT_BEGIN "opening %.*s library\n" INDENT_END(U_), (int) len_, name_));
-                STACK_CHECK_START_REL(L_, 0);
-                // open the library as if through require(), and create a global as well if necessary (the library table is left on the stack)
-                luaL_requiref(L_, name_, libfunc, !isLanesCore);
-                // lanes.core doesn't declare a global, so scan it here and now
-                if (isLanesCore == true) {
-                    populate_func_lookup_table(L_, -1, name_);
-                }
-                lua_pop(L_, 1);
-                STACK_CHECK(L_, 0);
+    for (int i{ 0 }; global::sLibs[i].name; ++i) {
+        if (strncmp(name_, global::sLibs[i].name, len_) == 0) {
+            lua_CFunction const libfunc{ global::sLibs[i].func };
+            if (!libfunc) {
+                continue;
+            }
+            name_ = global::sLibs[i].name; // note that the provided name_ doesn't necessarily ends with '\0', hence len_
+            DEBUGSPEW_CODE(fprintf(stderr, INDENT_BEGIN "opening %.*s library\n" INDENT_END(U_), (int) len_, name_));
+            STACK_CHECK_START_REL(L_, 0);
+            // open the library as if through require(), and create a global as well if necessary (the library table is left on the stack)
+            bool const isLanesCore{ libfunc == require_lanes_core }; // don't want to create a global for "lanes.core"
+            luaL_requiref(L_, name_, libfunc, !isLanesCore);                                       // L_: {lib}
+            // lanes.core doesn't declare a global, so scan it here and now
+            if (isLanesCore) {
+                populate_func_lookup_table(L_, -1, name_);
             }
+            lua_pop(L_, 1);                                                                        // L_:
+            STACK_CHECK(L_, 0);
             break;
         }
     }
@@ -180,6 +184,14 @@ static void open1lib(DEBUGSPEW_PARAM_COMMA(Universe* U_) lua_State* L_, char con
 
 // #################################################################################################
 
+template<size_t N>
+static inline void open1lib(DEBUGSPEW_PARAM_COMMA(Universe* U_) lua_State* L_, char const (&name_)[N])
+{
+    open1lib(DEBUGSPEW_PARAM_COMMA(U_) L_, name_, N - 1);
+}
+
+// #################################################################################################
+
 // just like lua_xmove, args are (from, to)
 static void copy_one_time_settings(Universe* U_, SourceState L1_, DestState L2_)
 {
@@ -195,7 +207,7 @@ static void copy_one_time_settings(Universe* U_, SourceState L1_, DestState L2_)
     // copy settings from from source to destination registry
     InterCopyContext c{ U_, L2_, L1_, {}, {}, {}, {}, {} };
     if (c.inter_move(1) != InterCopyResult::Success) {                                             // L1_:                                           L2_: config
-        raise_luaL_error(L1_, "failed to copy settings when loading lanes.core");
+        raise_luaL_error(L1_, "failed to copy settings when loading " kLanesCoreLibName);
     }
     // set L2:_R[kConfigRegKey] = settings
     kConfigRegKey.setValue(L2_, [](lua_State* L_) { lua_insert(L_, -2); });                        // L1_:                                           L2_: config
@@ -334,11 +346,10 @@ lua_State* luaG_newstate(Universe* U_, SourceState from_, char const* libs_)
     // copy settings (for example because it may contain a Lua on_state_create function)
     copy_one_time_settings(U_, from_, L);
 
-    // 'lua.c' stops GC during initialization so perhaps its a good idea. :)
+    // 'lua.c' stops GC during initialization so perhaps it is a good idea. :)
     lua_gc(L, LUA_GCSTOP, 0);
 
     // Anything causes 'base' to be taken in
-    //
     if (libs_ != nullptr) {
         // special "*" case (mainly to help with LuaJIT compatibility)
         // as we are called from luaopen_lanes_core() already, and that would deadlock
@@ -346,7 +357,7 @@ lua_State* luaG_newstate(Universe* U_, SourceState from_, char const* libs_)
             DEBUGSPEW_CODE(fprintf(stderr, INDENT_BEGIN "opening ALL standard libraries\n" INDENT_END(U_)));
             luaL_openlibs(L);
             // don't forget lanes.core for regular lane states
-            open1lib(DEBUGSPEW_PARAM_COMMA(U_) L, "lanes.core", 10);
+            open1lib(DEBUGSPEW_PARAM_COMMA(U_) L, kLanesCoreLibName);
             libs_ = nullptr; // done with libs
         } else {
             DEBUGSPEW_CODE(fprintf(stderr, INDENT_BEGIN "opening base library\n" INDENT_END(U_)));
diff --git a/src/tools.cpp b/src/tools.cpp
index 2d48552..73efda9 100644
--- a/src/tools.cpp
+++ b/src/tools.cpp
@@ -348,7 +348,7 @@ static void populate_func_lookup_table_recur(DEBUGSPEW_PARAM_COMMA(Universe* U_)
     while (lua_next(L_, breadthFirstCache) != 0) {                                                 // L_: ... {i_} {bfc} k {}
         DEBUGSPEW_CODE(char const* key = (lua_type(L_, -2) == LUA_TSTRING) ? lua_tostring(L_, -2) : "not a string");
         DEBUGSPEW_CODE(fprintf(stderr, INDENT_BEGIN "table '%s'\n" INDENT_END(U_), key));
-        DEBUGSPEW_CODE(DebugSpewIndentScope scope{ U_ });
+        DEBUGSPEW_CODE(DebugSpewIndentScope scope2{ U_ });
         // un-visit this table in case we do need to process it
         lua_pushvalue(L_, -1);                                                                     // L_: ... {i_} {bfc} k {} {}
         lua_rawget(L_, cache);                                                                     // L_: ... {i_} {bfc} k {} n
diff --git a/src/universe.cpp b/src/universe.cpp
index bf64560..6adc314 100644
--- a/src/universe.cpp
+++ b/src/universe.cpp
@@ -1,11 +1,11 @@
 /*
- * UNIVERSE.C                  Copyright (c) 2017, Benoit Germain
+ * UNIVERSE.CPP                  Copyright (c) 2017-2024, Benoit Germain
  */
 
 /*
 ===============================================================================
 
-Copyright (C) 2017 Benoit Germain <bnt.germain@gmail.com>
+Copyright (C) 2017-2024 Benoit Germain <bnt.germain@gmail.com>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -28,18 +28,14 @@ THE SOFTWARE.
 ===============================================================================
 */
 
-#include <string.h>
-#include <assert.h>
-
 #include "universe.h"
-#include "compat.h"
-#include "macros_and_utils.h"
-#include "uniquekey.h"
 
-// xxh64 of string "kUniverseFullRegKey" generated at https://www.pelock.com/products/hash-calculator
-static constexpr RegistryUniqueKey kUniverseFullRegKey{ 0x1C2D76870DD9DD9Full };
-// xxh64 of string "kUniverseLightRegKey" generated at https://www.pelock.com/products/hash-calculator
-static constexpr RegistryUniqueKey kUniverseLightRegKey{ 0x48BBE9CEAB0BA04Full };
+#include "cancel.h"
+#include "compat.h"
+#include "deep.h"
+#include "keeper.h"
+#include "lanes_private.h"
+#include "tools.h"
 
 // #################################################################################################
 
@@ -72,7 +68,7 @@ Universe::Universe()
 // #################################################################################################
 
 // only called from the master state
-Universe* universe_create(lua_State* L_)
+[[nodiscard]] Universe* universe_create(lua_State* L_)
 {
     LUA_ASSERT(L_, universe_get(L_) == nullptr);
     Universe* const U{ lua_newuserdatauv<Universe>(L_, 0) }; // universe
@@ -86,20 +82,94 @@ Universe* universe_create(lua_State* L_)
 
 // #################################################################################################
 
-void universe_store(lua_State* L_, Universe* U_)
+void Universe::terminateFreeRunningLanes(lua_State* L_, lua_Duration shutdownTimeout_, CancelOp op_)
 {
-    LUA_ASSERT(L_, !U_ || universe_get(L_) == nullptr);
-    STACK_CHECK_START_REL(L_, 0);
-    kUniverseLightRegKey.setValue(L_, [U = U_](lua_State* L_) { U ? lua_pushlightuserdata(L_, U) : lua_pushnil(L_); });
-    STACK_CHECK(L_, 0);
+    if (selfdestructFirst != SELFDESTRUCT_END) {
+        // Signal _all_ still running threads to exit (including the timer thread)
+        {
+            std::lock_guard<std::mutex> guard{ selfdestructMutex };
+            Lane* lane{ selfdestructFirst };
+            lua_Duration timeout{ 1us };
+            while (lane != SELFDESTRUCT_END) {
+                // attempt the requested cancel with a small timeout.
+                // if waiting on a linda, they will raise a cancel_error.
+                // if a cancellation hook is desired, it will be installed to try to raise an error
+                if (lane->thread.joinable()) {
+                    std::ignore = thread_cancel(lane, op_, 1, timeout, true);
+                }
+                lane = lane->selfdestruct_next;
+            }
+        }
+
+        // When noticing their cancel, the lanes will remove themselves from the selfdestruct chain.
+        {
+            std::chrono::time_point<std::chrono::steady_clock> t_until{ std::chrono::steady_clock::now() + std::chrono::duration_cast<std::chrono::steady_clock::duration>(shutdownTimeout_) };
+
+            while (selfdestructFirst != SELFDESTRUCT_END) {
+                // give threads time to act on their cancel
+                std::this_thread::yield();
+                // count the number of cancelled thread that didn't have the time to act yet
+                int n{ 0 };
+                {
+                    std::lock_guard<std::mutex> guard{ selfdestructMutex };
+                    Lane* lane{ selfdestructFirst };
+                    while (lane != SELFDESTRUCT_END) {
+                        if (lane->cancelRequest != CancelRequest::None)
+                            ++n;
+                        lane = lane->selfdestruct_next;
+                    }
+                }
+                // if timeout elapsed, or we know all threads have acted, stop waiting
+                std::chrono::time_point<std::chrono::steady_clock> t_now = std::chrono::steady_clock::now();
+                if (n == 0 || (t_now >= t_until)) {
+                    DEBUGSPEW_CODE(fprintf(stderr, "%d uncancelled lane(s) remain after waiting %fs at process end.\n", n, shutdownTimeout_.count()));
+                    break;
+                }
+            }
+        }
+
+        // If some lanes are currently cleaning after themselves, wait until they are done.
+        // They are no longer listed in the selfdestruct chain, but they still have to lua_close().
+        while (selfdestructingCount.load(std::memory_order_acquire) > 0) {
+            std::this_thread::yield();
+        }
+    }
+
+    // If after all this, we still have some free-running lanes, it's an external user error, they should have stopped appropriately
+    {
+        std::lock_guard<std::mutex> guard{ selfdestructMutex };
+        Lane* lane{ selfdestructFirst };
+        if (lane != SELFDESTRUCT_END) {
+            // this causes a leak because we don't call U's destructor (which could be bad if the still running lanes are accessing it)
+            raise_luaL_error(L_, "Zombie thread %s refuses to die!", lane->debugName);
+        }
+    }
 }
 
 // #################################################################################################
 
-Universe* universe_get(lua_State* L_)
+// process end: cancel any still free-running threads
+int universe_gc(lua_State* L_)
 {
-    STACK_CHECK_START_REL(L_, 0);
-    Universe* const universe{ kUniverseLightRegKey.readLightUserDataValue<Universe>(L_) };
-    STACK_CHECK(L_, 0);
-    return universe;
+    lua_Duration const shutdown_timeout{ lua_tonumber(L_, lua_upvalueindex(1)) };
+    [[maybe_unused]] char const* const op_string{ lua_tostring(L_, lua_upvalueindex(2)) };
+    Universe* const U{ lua_tofulluserdata<Universe>(L_, 1) };
+    U->terminateFreeRunningLanes(L_, shutdown_timeout, which_cancel_op(op_string));
+
+    // no need to mutex-protect this as all threads in the universe are gone at that point
+    if (U->timerLinda != nullptr) { // test in case some early internal error prevented Lanes from creating the deep timer
+        [[maybe_unused]] int const prev_ref_count{ U->timerLinda->refcount.fetch_sub(1, std::memory_order_relaxed) };
+        LUA_ASSERT(L_, prev_ref_count == 1); // this should be the last reference
+        DeepFactory::DeleteDeepObject(L_, U->timerLinda);
+        U->timerLinda = nullptr;
+    }
+
+    close_keepers(U);
+
+    // remove the protected allocator, if any
+    U->protectedAllocator.removeFrom(L_);
+
+    U->Universe::~Universe();
+
+    return 0;
 }
diff --git a/src/universe.h b/src/universe.h
index b2107af..58ddffc 100644
--- a/src/universe.h
+++ b/src/universe.h
@@ -11,12 +11,14 @@ extern "C"
 
 #include "compat.h"
 #include "macros_and_utils.h"
+#include "uniquekey.h"
 
 #include <mutex>
 
 // #################################################################################################
 
 // forwards
+enum class CancelOp;
 struct DeepPrelude;
 struct Keepers;
 class Lane;
@@ -114,6 +116,13 @@ class ProtectedAllocator
 
 // #################################################################################################
 
+// xxh64 of string "kUniverseFullRegKey" generated at https://www.pelock.com/products/hash-calculator
+static constexpr RegistryUniqueKey kUniverseFullRegKey{ 0x1C2D76870DD9DD9Full };
+// xxh64 of string "kUniverseLightRegKey" generated at https://www.pelock.com/products/hash-calculator
+static constexpr RegistryUniqueKey kUniverseLightRegKey{ 0x48BBE9CEAB0BA04Full };
+
+// #################################################################################################
+
 // everything regarding the Lanes universe is stored in that global structure
 // held as a full userdata in the master Lua state that required it for the first time
 class Universe
@@ -154,6 +163,7 @@ class Universe
     Lane* volatile trackingFirst{ nullptr }; // will change to TRACKING_END if we want to activate tracking
 #endif // HAVE_LANE_TRACKING()
 
+    // Protects modifying the selfdestruct chain
     std::mutex selfdestructMutex;
 
     // require() serialization
@@ -178,6 +188,8 @@ class Universe
     Universe(Universe&&) = delete;
     Universe& operator=(Universe const&) = delete;
     Universe& operator=(Universe&&) = delete;
+
+    void terminateFreeRunningLanes(lua_State* L_, lua_Duration shutdownTimeout_, CancelOp op_);
 };
 
 // #################################################################################################
@@ -211,3 +223,27 @@ class DebugSpewIndentScope
     }
 };
 #endif // USE_DEBUG_SPEW()
+
+// #################################################################################################
+
+[[nodiscard]] inline Universe* universe_get(lua_State* L_)
+{
+    STACK_CHECK_START_REL(L_, 0);
+    Universe* const universe{ kUniverseLightRegKey.readLightUserDataValue<Universe>(L_) };
+    STACK_CHECK(L_, 0);
+    return universe;
+}
+
+// #################################################################################################
+
+inline void universe_store(lua_State* L_, Universe* U_)
+{
+    LUA_ASSERT(L_, !U_ || universe_get(L_) == nullptr);
+    STACK_CHECK_START_REL(L_, 0);
+    kUniverseLightRegKey.setValue(L_, [U = U_](lua_State* L_) { U ? lua_pushlightuserdata(L_, U) : lua_pushnil(L_); });
+    STACK_CHECK(L_, 0);
+}
+
+// #################################################################################################
+
+int universe_gc(lua_State* L_);
-- 
cgit v1.2.3-55-g6feb