From 9808ae3e21ac812ef705a7c1a0b10f49825023c5 Mon Sep 17 00:00:00 2001
From: Benoit Germain <bnt period germain arrobase gmail period com>
Date: Wed, 22 Jan 2014 12:28:45 +0100
Subject: new lane launcher option gc_cb

* bumped version to 3.8.2
* new lane launcher option gc_cb to set a callback that is invoked when
a lane is garbage collected
* Fix more invalid memory accesses when fetching the name of a joined
lane with lanes:threads() (because its lua_State is closed)
---
 src/lanes.c   | 111 +++++++++++++++++++++++++++++++++++++++++++---------------
 src/lanes.lua |  14 +++++---
 2 files changed, 93 insertions(+), 32 deletions(-)

(limited to 'src')

diff --git a/src/lanes.c b/src/lanes.c
index 604e43d..a806c16 100644
--- a/src/lanes.c
+++ b/src/lanes.c
@@ -52,7 +52,7 @@
  *      ...
  */
 
-char const* VERSION = "3.8.1";
+char const* VERSION = "3.8.2";
 
 /*
 ===============================================================================
@@ -209,6 +209,19 @@ static inline struct s_lane* get_lane_from_registry( lua_State* L)
 	return s;
 }
 
+// intern the debug name in the specified lua state so that the pointer remains valid when the lane's state is closed
+static void securize_debug_threadname( lua_State* L, struct s_lane* s)
+{
+	STACK_CHECK( L);
+	STACK_GROW( L, 3);
+	lua_getuservalue( L, 1);
+	lua_newtable( L);
+	s->debug_name = lua_pushstring( L, s->debug_name);
+	lua_rawset( L, -3);
+	lua_pop( L, 1);
+	STACK_END( L, 0);
+}
+
 /*
 * Check if the thread in question ('L') has been signalled for cancel.
 *
@@ -1766,7 +1779,7 @@ LUAG_FUNC( get_debug_threadname)
 {
 	struct s_lane* const s = lua_toLane( L, 1);
 	luaL_argcheck( L, lua_gettop( L) == 1, 2, "too many arguments");
-	lua_pushstring( L, s->debug_name ? s->debug_name : "<unnamed>");
+	lua_pushstring( L, s->debug_name);
 	return 1;
 }
 
@@ -1998,6 +2011,8 @@ LUAG_FUNC( require)
 	return 1;
 }
 
+LUAG_FUNC( thread_gc);
+#define GCCB_KEY (void*)LG_thread_gc
 //---
 // lane_ud= thread_new( function, [libs_str], 
 //                          [cancelstep_uint=0], 
@@ -2005,6 +2020,7 @@ LUAG_FUNC( require)
 //                          [globals_tbl],
 //                          [package_tbl],
 //                          [required],
+//                          [gc_cb],
 //                          [... args ...] )
 //
 // Upvalues: metatable to use for 'lane_ud'
@@ -2022,8 +2038,9 @@ LUAG_FUNC( thread_new)
 	uint_t glob = lua_isnoneornil( L, 5) ? 0 : 5;
 	uint_t package = lua_isnoneornil( L, 6) ? 0 : 6;
 	uint_t required = lua_isnoneornil( L, 7) ? 0 : 7;
+	uint_t gc_cb = lua_isnoneornil( L, 8) ? 0 : 8;
 
-#define FIXED_ARGS 7
+#define FIXED_ARGS 8
 	uint_t args = lua_gettop(L) - FIXED_ARGS;
 
 	// public Lanes API accepts a generic range -3/+3
@@ -2203,7 +2220,7 @@ LUAG_FUNC( thread_new)
 	}
 	STACK_MID( L, 0);
 
-	ASSERT_L( (uint_t)lua_gettop( L2) == 1+args);
+	ASSERT_L( (uint_t)lua_gettop( L2) == 1 + args);
 	ASSERT_L( lua_isfunction( L2, 1));
 
 	// 's' is allocated from heap, not Lua, since its life span may surpass 
@@ -2217,9 +2234,9 @@ LUAG_FUNC( thread_new)
 
 	//memset( s, 0, sizeof(struct s_lane) );
 	s->L = L2;
-	s->status= PENDING;
+	s->status = PENDING;
 	s->waiting_on = NULL;
-	s->debug_name = NULL;
+	s->debug_name = "<unnamed>";
 	s->cancel_request = CANCEL_NONE;
 
 #if THREADWAIT_METHOD == THREADWAIT_CONDVAR
@@ -2238,25 +2255,32 @@ LUAG_FUNC( thread_new)
 	lua_setmetatable( L, -2);
 	STACK_MID( L, 1);
 
-	// Clear environment for the userdata
-	//
+	// Create uservalue for the userdata
+	// (this is where lane body return values will be stored when the handle is indexed by a numeric key)
 	lua_newtable( L);
+
+	// Store the gc_cb callback in the uservalue
+	if( gc_cb > 0)
+	{
+		lua_pushlightuserdata( L, GCCB_KEY);
+		lua_pushvalue( L, gc_cb);
+		lua_rawset( L, -3);
+	}
+
 	lua_setuservalue( L, -2);
 
-	// Place 's' in registry, for 'cancel_test()' (even if 'cs'==0 we still
-	// do cancel tests at pending send/receive).
-	//
+	// Store 's' in the lane's registry, for 'cancel_test()' (even if 'cs'==0 we still do cancel tests at pending send/receive).
 	lua_pushlightuserdata( L2, CANCEL_TEST_KEY);
 	lua_pushlightuserdata( L2, s);
 	lua_rawset( L2, LUA_REGISTRYINDEX);
 
 	if( cs)
 	{
-		lua_sethook( L2, cancel_hook, LUA_MASKCOUNT, cs );
+		lua_sethook( L2, cancel_hook, LUA_MASKCOUNT, cs);
 	}
 
 	DEBUGSPEW_CODE( fprintf( stderr, INDENT_BEGIN "thread_new: launching thread\n" INDENT_END));
-	THREAD_CREATE( &s->thread, lane_main, s, prio );
+	THREAD_CREATE( &s->thread, lane_main, s, prio);
 	STACK_END( L, 1);
 
 	DEBUGSPEW_CODE( -- debugspew_indent_depth);
@@ -2279,7 +2303,23 @@ LUAG_FUNC( thread_new)
 //
 LUAG_FUNC( thread_gc)
 {
-	struct s_lane* s = lua_toLane( L, 1);
+	bool_t have_gc_cb = FALSE;
+	struct s_lane* s = lua_toLane( L, 1);                               // ud
+
+	// if there a gc callback?
+	lua_getuservalue( L, 1);                                            // ud uservalue
+	lua_pushlightuserdata( L, GCCB_KEY);                                // ud uservalue __gc
+	lua_rawget( L, -2);                                                 // ud uservalue gc_cb|nil
+	if( !lua_isnil( L, -1))
+	{
+		lua_remove( L, -2);                                               // ud gc_cb|nil
+		lua_pushstring( L, s->debug_name);                                // ud gc_cb name
+		have_gc_cb = TRUE;
+	}
+	else
+	{
+		lua_pop( L, 2);                                                   // ud
+	}
 
 	// We can read 's->status' without locks, but not wait for it
 	// test KILLED state first, as it doesn't need to enter the selfdestruct chain
@@ -2291,13 +2331,17 @@ LUAG_FUNC( thread_gc)
 		DEBUGSPEW_CODE( fprintf( stderr, "** Joining with a killed thread (needs testing) **"));
 		// make sure the thread is no longer running, just like thread_join()
 		if(! THREAD_ISNULL( s->thread))
+		{
 			THREAD_WAIT( &s->thread, -1, &s->done_signal, &s->done_lock, &s->status);
-		// we know the thread was killed while the Lua VM was not doing anything: we should be able to close it without crashing
-		// now, thread_cancel() will not forcefully kill a lane with s->status >= DONE, so I am not sure it can ever happen
+		}
 		if( s->status >= DONE && s->L)
 		{
+			// we know the thread was killed while the Lua VM was not doing anything: we should be able to close it without crashing
+			// now, thread_cancel() will not forcefully kill a lane with s->status >= DONE, so I am not sure it can ever happen
 			lua_close( s->L);
 			s->L = 0;
+			// just in case, but s will be freed soon so...
+			s->debug_name = "<gc>";
 		}
 		DEBUGSPEW_CODE( fprintf( stderr, "** Joined ok **"));
 	}
@@ -2306,18 +2350,31 @@ LUAG_FUNC( thread_gc)
 		// still running: will have to be cleaned up later
 		selfdestruct_add( s);
 		assert( s->selfdestruct_next);
+		if( have_gc_cb)
+		{
+			lua_pushliteral( L, "selfdestruct");                            // ud gc_cb name status
+			lua_call( L, 2, 0);                                             // ud
+		}
 		return 0;
-
 	}
 	else if( s->L)
 	{
 		// no longer accessing the Lua VM: we can close right now
 		lua_close( s->L);
 		s->L = 0;
+		// just in case, but s will be freed soon so...
+		s->debug_name = "<gc>";
 	}
 
 	// Clean up after a (finished) thread
 	lane_cleanup( s);
+
+	// do this after lane cleanup in case the callback triggers an error
+	if( have_gc_cb)
+	{
+		lua_pushliteral( L, "closed");                                    // ud gc_cb name status
+		lua_call( L, 2, 0);                                               // ud
+	}
 	return 0;
 }
 
@@ -2422,23 +2479,26 @@ LUAG_FUNC( thread_join)
 	bool_t done;
 
 	done = THREAD_ISNULL( s->thread) || THREAD_WAIT( &s->thread, wait_secs, &s->done_signal, &s->done_lock, &s->status);
-	if (!done || !L2)
+	if( !done || !L2)
+	{
 		return 0;      // timeout: pushes none, leaves 'L2' alive
+	}
 
 	// Thread is DONE/ERROR_ST/CANCELLED; all ours now
 
-	STACK_GROW( L, 1);
-
 	if( s->mstatus == KILLED) // OS thread was killed if thread_cancel was forced
 	{
 		// in that case, even if the thread was killed while DONE/ERROR_ST/CANCELLED, ignore regular return values
-		
+		STACK_GROW( L, 1);
 		lua_pushnil( L);
 		lua_pushliteral( L, "killed");
 		ret = 2;
 	}
 	else
 	{
+		// debug_name is a pointer to string possibly interned in the lane's state, that no longer exists when the state is closed
+		// so store it in the userdata uservalue at a key that can't possibly collide
+		securize_debug_threadname( L, s);
 		switch( s->status)
 		{
 			case DONE:
@@ -2467,11 +2527,9 @@ LUAG_FUNC( thread_join)
 
 			default:
 			DEBUGSPEW_CODE( fprintf( stderr, "Status: %d\n", s->status));
-			ASSERT_L( FALSE ); ret= 0;
+			ASSERT_L( FALSE); ret = 0;
 		}
 		lua_close( L2);
-		// debug_name is a pointer to an interned string, that no longer exists when the state is closed
-		s->debug_name = "<closed>";
 	}
 	s->L = 0;
 
@@ -2652,10 +2710,7 @@ LUAG_FUNC( threads)
 		lua_newtable( L);                                          // {}
 		while( s != TRACKING_END)
 		{
-			if( s->debug_name)
-				lua_pushstring( L, s->debug_name);                     // {} "name"
-			else
-				lua_pushfstring( L, "Lane %p", s);                     // {} "name"
+			lua_pushstring( L, s->debug_name);                       // {} "name"
 			push_thread_status( L, s);                               // {} "name" "status"
 			lua_rawset( L, -3);                                      // {}
 			s = s->tracking_next;
diff --git a/src/lanes.lua b/src/lanes.lua
index 9a0287d..1286099 100644
--- a/src/lanes.lua
+++ b/src/lanes.lua
@@ -196,7 +196,10 @@ end
 --
 --        .globals:  table of globals to set for a new thread (passed by value)
 --
---        .required:  table of packages to require
+--        .required: table of packages to require
+--
+--        .gc_cb:    function called when the lane handle is collected
+--
 --        ... (more options may be introduced later) ...
 --
 -- Calling with a function parameter ('lane_func') ends the string/table
@@ -272,10 +275,11 @@ local function gen( ... )
         end
     end
     
-    local prio, cs, g_tbl, package_tbl, required
+    local prio, cs, g_tbl, package_tbl, required, gc_cb
 
     for k,v in pairs(opt) do
-            if k=="priority" then prio= v
+        if k == "priority" then
+            prio = (type( v) == "number") and v or error( "Bad 'prio' option: expecting number, got " .. type( v), lev)
         elseif k=="cancelstep" then
             cs = (v==true) and 100 or
                 (v==false) and 0 or 
@@ -286,6 +290,8 @@ local function gen( ... )
             package_tbl = (type( v) == "table") and v or error( "Bad package: " .. tostring( v), lev)
         elseif k=="required" then
             required= (type( v) == "table") and v or error( "Bad 'required' option: expecting table, got " .. type( v), lev)
+        elseif k == "gc_cb" then
+            gc_cb = (type( v) == "function") and v or error( "Bad 'gc_cb' option: expecting function, got " .. type( v), lev)
         --..
         elseif k==1 then error( "unkeyed option: ".. tostring(v), lev )
         else error( "Bad option: ".. tostring(k), lev )
@@ -296,7 +302,7 @@ local function gen( ... )
     -- Lane generator
     --
     return function(...)
-        return thread_new( func, libs, cs, prio, g_tbl, package_tbl, required, ...)     -- args
+        return thread_new( func, libs, cs, prio, g_tbl, package_tbl, required, gc_cb, ...)     -- args
     end
 end
 
-- 
cgit v1.2.3-55-g6feb