Cloning a function in Lua

Have you ever considered whether functions in Lua are mutable or not. In Lua, objects are mutable because properties and metatables can be changed. Strings and numbers are examples of types that aren’t mutable: string library functions return new strings, numeric operators return a new numbers.

Functions and mutability

For something to be mutable it must have state that can be changed. Depending on the version of Lua, there are either two possibilities:

  • Lua 5.1: The function environment and the upvalues are mutable
  • Lua 5.2 and above: the upvalues are mutable

Lua 5.2 replaced the function environment with a specially named upvalue called _ENV. You can read more about this in my companion guide: Implementing setfenv in Lua.

When assigning a function to a new variable it is not copied. Just like tables, function values are actually pointers to a function.

local a = function() end
local b = a

-- these point to the same function
assert(a == b)

This is commonly confused with pass by reference. Pass by reference is slightly different. Lua uses pass by value, it’s just that some values are pointers to the same object.

Why clone a function?

A cloned function will allow you to change state without affecting other code that is holding references to the original function.

You might think that because Lua is a single threaded language you can modify the state of the function while it executes, then put it back. This would be true if Lua didn’t have coroutines.

A running function might yield at any point in execution, and in that time the function could have its state changed before the coroutine resumes.

string.dump and loadstring

The string.dump function returns a binary representation of a function as a string. By dumping a function to a string and then reloading it you've created a clone of the function:

local function say_hi()
  print("Hi!")
end

local say_hi_clone = loadstring(string.dump(say_hi))

say_hi_clone() --> Hi!

assert(say_hi ~= say_hi_clone)

This works in the previous example but it’s not entirely correct. What about upvalues? An upvalue’s reference can not be encoded into the string dump and preserved when it’s loaded again.

local message = "Hello"

local function say_message()
  print("message: " .. tostring(message))
end

local say_message_clone = loadstring(string.dump(say_message))

say_message_clone() -- message: nil

A new set of upvalues is created for the loaded function, and they all point to nil.

Preserving upvalues

Lua 5.2 and above give two ways to set upvalues on a function: debug.setupvalue and debug.upvaluejoin. As we discovered in the setfenv implementation guide, upvalues are shared among multiple functions. Changes to the values pointed to by an upvalue should reflect in all the functions that have access. For that reason debug.upvaluejoin will be used to connect the original function’s upvalues to the new function.

debug.upvaluejoin takes two pairs of function and upvalue index. Since one function is a clone of the other, the upvalue positions will be the same. It’s just a matter of iterating through all the valid upvalue indexes and joining them.

local message = "Hello"

local function say_message()
  print("message: " .. tostring(message))
end

local say_message_clone = loadstring(string.dump(say_message))


local i = 1
while true do
  -- see if i is a valid upvalue index
  local name = debug.getupvalue(say_message, i)
  if not name then
    break
  end
  -- join the clone and the original
  debug.upvaluejoin(say_message_clone, i, say_message, i)
  i = i + 1
end

-- the clone now has a functional upvalue
say_message_clone() -- message: Hello
message = "MoonScript"
say_message_clone() -- message: MoonScript

clone_function implementation

Now all that’s left is to write a generic function for cloning any function:

local function clone_function(fn)
  local dumped = string.dump(fn)
  local cloned = loadstring(dumped)
  local i = 1
  while true do
    local name = debug.getupvalue(fn, i)
    if not name then
      break
    end
    debug.upvaluejoin(cloned, i, fn, i)
    i = i + 1
  end
  return cloned
end

Handling Lua 5.1

As far as I know Lua 5.1 does not provide a way to join upvalues. LuaJIT does provite an implementation of debug.upvaluejoin though, so that may handle any Lua runtimes you run code in.

The best alternative for Lua 5.1 is to use debug.setupvalue. The result will be a function that works, but because the upvalues aren’t connected some hard to debug issues may result.