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:
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 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
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
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
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.