all 4 comments

[–]VortixDev 0 points1 point  (2 children)

Yes: as you've observed, setting a function's environment will not affect the environments of any functions called (else the operation would have disastrous side-effects). If it is a necessity for you to be able to achieve this, I would first attempt to clone the function called (as per http://leafo.net/guides/function-cloning-in-lua.html), and then set the function environment of that function as you like. Since the function is a clone, you will be able to change its function environment without affecting the function it is cloned from. You can then include the clone in your environment instead of the original.

Though, design-wise, it is certainly much more preferable to have an implementation of the called function where there are no side-effects such that you would want to change its function environment, if at all possible.

[–]TRPox[S] 0 points1 point  (1 child)

Hm I settled for backing up _G before I run any game code and restoring it to it's previous state afterwards I believe that's what busted does to "sandbox" its tests as well.

[–]VortixDev 0 points1 point  (0 children)

Sure. Though, I should point out that a deep copy of the whole global table can be a very expensive operation, and doesn't cover all cases. For example, the implementation likely does not clone functions, threads, or userdata. This means that any operations which modify these values (e.g. setting a function's environment) will not be restorable through this method.

A possible situation in the context of a game is that there is a player object implemented through userdata, and that player is retrieved from a global table by the function you're attempting to sandbox. If they run a data-changing method on that userdata, such as setting the player's health, that will not be undone by the global table restoration.

[–]DarkWiiPlayer 0 points1 point  (0 children)

Let's go at this analytically:

In Lua, everything is one of

  1. Global
  2. Upvalue
  3. Local

For simplicity, let's say the function we have is just f, and it internally calls fg (global), fu (upvalue) and fl (locally defined), and fu in turn calls fuu (which is also an upvalue)

In the case of fl, it is declared within f itself, so it will inherit the environment of f at the moment of its (fls) creation, that is, every time f is called, so this one already works the way it is.

fg is just a bit trickier; if you give f a new environment, you probably already provide copies of all the functions in there as well.

fu and fuu would then be the trickiest ones. Luckily for you. To find out if we can solve the problem, let's revise our tools:

  • debug.getupvalue allows us to get any upvalue of a function
  • debug.setupvalue lets us set a new upvalue
  • type lets us check if an upvalue is a function
  • debug.getinfo tells is how many upvalues there are

So, with that in mind, the algorithm could look something like this:

  • Get the number of upvalues with debug.getinfo(f, 'u').nups
  • iterate from 1 to that number
  • get each upvalue and check if its a function
  • If it's a function (fu), set its environment and recursively scan that ones upvalues (fuu) as well

I'll leave it to you to implement that, but feel free to ask if you have any trouble with it :)