Implementing setfenv in Lua 5.2, 5.3, and above

An upvalue tutorial

If you've programmed in Lua 5.1 you've probably come across the function setfenv (and the associated getfenv). Lua 5.2 removed those functions favor of the _ENV variable. The _ENV variable is an interesting change, and can accomplish many of the same things, but it is not a direct replacement.

View the completed implementation or read on to learn about how it’s created.

The _ENV variable

The _ENV variable provides a lexical way of setting a function’s environment. In the sense that functions have access to lexically scoped variables created a closure, a function’s environment is set by the closest lexically set value of _ENV.

Free variables referenced by closures are called upvalues. The _ENV variable is just an upvalue of a function that’s has the special purpose of being the place where global variable references are searched.

In order to re-implement setfenv, and set the function’s environment, that upvalue must be updated to point to a table to act as the environment.

Getting upvalues

Upvalues can be accessed and manipulated in Lua by using the debug functions debug.getupvalue, debug.setupvalue, and debug.upvaluejoin.

Lua upvalues are indexed by an integer, much like an array. The debug function debug.getupvalue(func, index) is used to access them.

Before implementing setfenv we'll examine a function’s upvalues. Here’s a basic function:

local number = 15
local function lucky()
  print("your lucky number: " .. number)
end

There is one local variable closed on the function, the variable number.

Here’s a chunk of code that will print out the upvalues. It works by enumerating positive integers until nil is returned by debug.getupvalue.

-- print out the upvalues of the function lucky
local idx = 1
while true do
  local name, val = debug.getupvalue(lucky, idx)
  if not name then break end
  print(name, val)
  idx = idx + 1
end

This will produce something like:

_ENV    table: 0x11e39f0
number  15

There are two upvalues, _ENV and number. What’s the default value of _ENV? It’s equal to the global variable _G.

As a quick example, how many upvalues does this function have? (note the absence of local on number)

number = 15

local function lucky2()
  print("your lucky number: " .. number)
end

Here’s the output:

_ENV    table: 0x11e39f0

number is no longer an upvalue because it is now a global variable. When number is referenced inside of that function it will look within the _ENV upvalue, effectively the value of _ENV.number.

One last example, what’s the upvalue of this (empty) function:

local function no_so_lucky()
end

You might be thinking “Well, just _ENV right?”. In reality this function has no upvalues. A function only has an upvalue on _ENV if it references a global variable. In the first example, the function referenced print globally, and the second example referenced both print and number. This is worth keeping in mind if you're ever trying to debug upvalue code.

Setting upvalues

The function debug.setupvalue(func, index, value) sets an upvalue for a function. As mentioned above, upvalues are referenced by their index. In order to set an upvalue by name, the index first must be found.

Here is the snippet above generalized to return a single upvalue’s index by its name:

local function get_upvalue(fn, search_name)
  local idx = 1
  while true do
    local name, val = debug.getupvalue(fn, idx)
    if not name then break end
    if name == search_name then
      return idx, val
    end
    idx = idx + 1
  end
end

Now it’s trivial to change an upvalue of the example function:

local number = 15
local function lucky()
  print("your lucky number: " .. number)
end

debug.setupvalue(get_upvalue(lucky, "number"), 22)

lucky() --> your lucky number: 22

Taking it a step further you might the upvalue _ENV to replace print with something else.

debug.setupvalue(lucky, get_upvalue(lucky, "_ENV"), {
  print = function()
    print("no lucky number for you!")
  end
})

lucky()
-- uhhhh....

This produces a stack overflow error, there’s an infinite loop within print. It kinda makes sense, we did just re-define print to call print with our new environment.

The problem is that upvalues can be shared across many functions. When we set the upvalue on lucky we actually changed every function environment. We changed the global environment to point to a function that just has print in it.

If print is called on the top level, instead of calling lucky, and the same infinite loop will happen. This is because the global scope also shared the same _ENV.

Additionally, calling any other regularly available global function, like type, will fail since the new environment only provides print.

Here’s another example:

local number = 15
local function lucky()
  print("your lucky number: " .. number)
end

local function lucky2()
  print("your other number: " .. number)
end

debug.setupvalue(lucky, get_upvalue(lucky, "number"), 22)
lucky2() --> your other number: 22

There are two functions that refer to the same upvalue number. Calling debug.setupvalue in this case has the same effect as writing number = 22, which changes what the results of both of the functions.

Since debug.setupvalue doesn’t work we'll need a new approach.

Replacing upvalues

The debug.upvaluejoin function replaces what an upvalue refers to. You must specify another function and upvalue index to do the replacement.

By creating a new anonymous function with and upvalue set to what _ENV should be in lucky, the upvalue can be replaced without affecting everything else.

local new_env = {
  print = function()
    print("no lucky number for you!")
  end
}

debug.upvaluejoin(lucky, get_upvalue(lucky, "_ENV"),
  function() return new_env end, 1)

lucky() --> prints "no lucky number for you"

The last two arguments reference upvalue 1 of the anonymous function. Since the anonymous function only has one upvalue it’s guaranteed to be at index 1. It’s not necessary to look it up by name.

Now we're ready for a completed setfenv implementation.

setfenv implementation

local function setfenv(fn, env)
  local i = 1
  while true do
    local name = debug.getupvalue(fn, i)
    if name == "_ENV" then
      debug.upvaluejoin(fn, i, (function()
        return env
      end), 1)
      break
    elseif not name then
      break
    end

    i = i + 1
  end

  return fn
end

getfenv implementation

local function getfenv(fn)
  local i = 1
  while true do
    local name, val = debug.getupvalue(fn, i)
    if name == "_ENV" then
      return val
    elseif not name then
      break
    end
    i = i + 1
  end
end