2

It is my understanding that in Lua 5.2 that environments are stored in upvalues named _ENV. This has made it really confusing for me to modify the environment of a chunk before running it, but after loading it.

I would like to load a file with some functions and use the chunk to inject those functions into various environments. Example:

chunk = loadfile( "file" )

-- Inject chunk's definitions
chunk._ENV = someTable -- imaginary syntax
chunk( )

chunk._ENV = someOtherTable
chunk( )

Is this possible from within Lua? The only examples I can find of modifying this upvalue are with the C api (another example from C api), but I am trying to do this from within Lua. Is this possible?

Edit: I'm unsure of accepting answers using the debug library. The docs state that the functions may be slow. I'm doing this for efficiency so that entire chunks don't have to be parsed from strings (or a file, even worse) just to inject variable definitions into various environments.

Edit: Looks like this is impossible: Recreating setfenv() in Lua 5.2

Edit: I suppose the best way for me to do this is to bind a C function that can modify the environment. Though this is a much more annoying way of going about it.

Edit: I believe a more natural way to do this would be to load all chunks into separate environments. These can be "inherited" by any other environment by setting a metatable that refers to a global copy of a chunk. This does not require any upvalue modification post-load, but still allows for multiple environments with those function definitions.

Community
  • 1
  • 1
RandyGaul
  • 1,915
  • 14
  • 21
  • possible duplicate of [Recreating setfenv() in Lua 5.2](http://stackoverflow.com/questions/14290527/recreating-setfenv-in-lua-5-2) – RandyGaul Dec 23 '13 at 00:46
  • 2
    Note that in the link you inserted, there's a [comment by one of Lua authors](http://stackoverflow.com/questions/14290527/recreating-setfenv-in-lua-5-2?rq=1#comment19850353_14290609) – akavel Dec 23 '13 at 01:06
  • I see the comment but I hardly know what he means. I don't have experience from 5.1 to understand. @akavel – RandyGaul Dec 23 '13 at 01:21
  • uh, actually I'm also not 100% sure, but from what I understand, in practice this means roughly what you seem to have gotten from others already: that it's doable via debug lib or C API (and in fact, debug does use the C API), albeit made kinda difficult and nonobvious on purpose, so that it's not overused in regular day-to-day Lua code. And coming from lhf, the answer has high credibility and gives insight as to the original intent. – akavel Dec 23 '13 at 09:28

3 Answers3

5

The simplest way to allow a chunk to be run in different environments is to make this explicit and have it receive an environment. Adding this line at the top of the chunk achieves this:

_ENV=...

Now you can call chunk(env1) and later chunk(env2) at your pleasure.

There, no debug magic with upvalues.

Although it will be clear if your chunk contains that line, you can add it at load time, by writing a suitable reader function that first sends that line and then the contents of the file.

lhf
  • 70,581
  • 9
  • 108
  • 149
  • Thanks a lot for the answer. It does seem quite nice, but adding that line to files is poor for scripting. So the comment about inserting this line just before reading a file is really cool. I am going to go try this out. – RandyGaul Dec 23 '13 at 21:54
1

I do not understand why you want to avoid using the debug library, while you are happy to use a C function (neither is possible in a sandbox.)

It can be done using debug.upvaluejoin:

function newEnvForChunk(chunk, index)
  local newEnv = {}
  local function source() return newEnv end
  debug.upvaluejoin(chunk, 1, source, 1)
  if index then setmetatable(newEnv, {__index=index}) end
  return newEnv
end

Now load any chunk like this:

local myChunk = load "print(x)"

It will initially inherit the enclosing _ENV. Now give it a new one:

local newEnv = newEnvForChunk(myChunk, _ENV)

and insert a value for 'x':

newEnv.x = 99

Now when you run the chunk, it should see the value for x:

myChunk()

=> 99

finnw
  • 47,861
  • 24
  • 143
  • 221
  • Oh is it really an okay thing to do, to use the Debug library? I'm not actually sandboxing for a test environment, I'm doing this for a release build. Specifically I am using this in a game engine where separate environments act as components in an aggregate based model. Each component environment should have definitions of `Init`, `Update` and `Shutdown`. Ideally all local variables in each "component file" can be instanced into separate environments, which is why I was wanting to use chunks to inject into environments. Do you think there is a better approach, or is this acceptable? – RandyGaul Dec 23 '13 at 02:25
  • I also slightly edited my question where I said I didn't want to accept debug library answers. Also can `debug.setupvalue` be used? It seems like this might be a lot simpler. – RandyGaul Dec 23 '13 at 02:39
  • If it helps, I tried out `debug.setupvalue` and it worked perfectly! I'll accept your answer. – RandyGaul Dec 23 '13 at 03:42
  • @RandyGaul If you have multiple chunks sharing the same `_ENV` (which is the default) then `debug.setupvalue` will affect all of them. If you create independent `_ENV`s (with `debug.upvaluejoin`) then they will not affect each other. – finnw Dec 23 '13 at 04:22
  • @RandyGaul re debug functions begin slow: yes they are, but so is loading a new chunk. And both are done only once at startup. – finnw Dec 23 '13 at 04:24
0

If you don't want to modify your chunk (per LHF's great answer) here are two alternatives:

Set up a blank environment, then dynamically change its environment to yours

function compile(code)
   local meta = {}
   local env = setmetatable({},meta)
   return {meta=meta, f=load('return '..code, nil, nil, env)}
end

function eval(block, scope)
   block.meta.__index=scope
   return block.f()
end

local block = compile('a + b * c')
print(eval(block, {a=1, b=2, c=3})) --> 7
print(eval(block, {a=2, b=3, c=4})) --> 14

Set up a blank environment, and re-set its values with your own each time

function compile(code)
   local env = {}
   return {env=env, f=load('return '..code, nil, nil, env)}
end

function eval(block, scope)
   for k,_ in pairs(block.env) do block.env[k]=nil end
   for k,v in pairs(scope) do block.env[k]=v end
   return block.f()
end

local block = compile('a + b * c')
print(eval(block, {a=1, b=2, c=3})) --> 7
print(eval(block, {a=2, b=3, c=4})) --> 14

Note that if micro-optimizations matter, the first option is about 2✕ as slow as the _ENV=... answer, while the second options is about 8–9✕ as slow.

Phrogz
  • 296,393
  • 112
  • 651
  • 745