3

I want to load some functions on a lua state, and then be able to invoke the functions from lua threads. I am trying to setfenv on the threads, so that the variables created by them are confined to the threads and not appear in the global env.

lua_State *L = luaL_newstate();
luaL_openlibs(L);

dostring(L, "function f1() my_var = 100 print('var set') end");/* create func on state */
/* ^-- a wrapper which does loadstring + pcall with error handling */

lua_State *l1 = lua_newthread(L);

lua_pushthread(l1);              /* l1: t                               */
lua_newtable(l1);                /* l1: t T1{}                          */
lua_newtable(l1);                /* l1: t T1{} T2{}                     */
lua_getglobal(l1, "_G");         /* l1: t T1{} T2{} _G                  */
lua_setfield(l1, -2, "__index"); /* l1: t T1{} T2{} ( T2.__index = _G)  */
lua_setmetatable(l1, -2);        /* l1: t T1 ( T1{}.mt = T2 )           */
if (!lua_setfenv(l1, -2))        /* l1: t (t.fenv = T1)                 */
   printf("setfenv fail!\n"); 
lua_pop(l1, 1);

dostring(l1, "print('l1: ', my_var)");       /* --> nil (expected) */
dostring(l1, "f1()  print('l1: ', my_var)"); /* --> l1: 100  (ok)  */
dostring(L, "print('L: ', my_var)");         /* --> L:  100  (No!) */

Am I doing anything wrong here ? (I don't want to load the function on the threads, because there can be a lot of them, and loading them once on the state seems to be the right approach)

--Edit--

The solution, seems to be:

  • create a new env table for each thread (with __index = _G)
  • for each function which runs within it, do setfenv(f1, getfenv(0))
finnw
  • 47,861
  • 24
  • 143
  • 221
Ani
  • 1,448
  • 1
  • 16
  • 38

1 Answers1

3

Each function has its own fenv. f1's fenv is _G, so when called (no matter which thread it is called in), it sets the global variable in _G. One option is to explicitly reference the thread environment from f1 e.g.

function f1()
  local env = getfenv(0)
  env.my_var = 100
  print('var set')
end

Another is to give each thread a private copy of f1.

A third option is to create a proxy fenv (the same one for all threads & functions) with __index and __newindex metamethods that delegate to the current thread environment (i.e. getfenv(0).):

-- Step 1: Create the shared proxy object that delegates to the
-- current thread environment.
local tlproxy = {} -- Always empty
local tlproxy_mt = {}

function tlproxy_mt:__index(k)
  return getfenv(0)[k]
end

function tlproxy_mt:__newindex(k, v)
  getfenv(0)[k] = v
end

setmetatable(tlproxy, tlproxy_mt)

-- Step 2: Give each new thread a new, empty environment table.
local tenv_mt = {}
tenv_mt.__index = _G -- allows access to _G.math etc.

local function createThread(f)
  local thread = coroutine.create(f)
  -- These functions will not work as expected if called from the new thread,
  -- so disable them.
  local tenv = {
    load=false, loadfile=false, loadstring=false,
    module=false, require=false
  }
  setmetatable(tenv, tenv_mt)
  debug.setfenv(thread, tenv)
  return thread
end

-- Step 3: When a function should use thread-local variables, it should be
-- given 'tlproxy' as its fenv.
function f1()
  my_var = 0
  while true do
    my_var = my_var + 1
    coroutine.yield(my_var)
  end
end
setfenv(f1, tlproxy)

local c1 = createThread(f1)
local c2 = createThread(f1)

-- Output should be 1, 1, 2, 2...
-- Without thread-locals it would be 1, 2, 3, 4...
for _ = 1, 100 do
  print(coroutine.resume(c1))
  print(coroutine.resume(c2))
end
                                                              52,1          Bot
finnw
  • 47,861
  • 24
  • 143
  • 221
  • so from your explanation, IIUC, I cannot just `setfenv` on the thread, but rather do that, for _each function_, before its invocation - is that correct ? (the reason I didn't want to take that approach is, because, there can be many functions). Is there any way to fake the _set_ (via metatables or some other way), so that the variables set in the thread env, do not get set in global env? – Ani Jul 10 '13 at 13:21
  • @vyom: Yes, I imagine you could have a proxy environment with `__index` and `__newindex` metamethods that refer to the current thread's environment. That way you would not need to modify the functions. I expect the performance cost would be high however. – finnw Jul 10 '13 at 14:51
  • I will try that. It would have been good to set an env for a thread _as a whole_ (including all the functions which run _in it_) because, now I have to do 2 steps, associate the `fenv` with the thread, and, associate the same `fenv` with the functions which run within it!. And as you mention, the cost is higher (in terms of performance, as well as time!) – Ani Jul 11 '13 at 09:03
  • If you use the metamethods, you only need to set the function fenv once. – finnw Jul 11 '13 at 13:24
  • Each time I invoke a function, I need to set its fenv (to that of the thread, in which it is running - because it can get called from multiple threads). I dont see how using metamethods can avoid this – Ani Jul 12 '13 at 07:12
  • The metamethod would use `coroutine.running` to discover which thread it was running in and delegate to a table associated with the thread. So though there would indeed be a table per thread, only one fenv is needed. – finnw Jul 12 '13 at 16:03
  • @finnw it's probably worth consolidating some of the comments into the answer. – greatwolf Jul 12 '13 at 19:59