leafo.net

My projects

View more →

Recent guides

View all →

Recent posts

Cloning a function in Lua

Posted July 08, 2015 by leafo (@moonscript) · Tags: lua
Tweet

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.

locala=function()end
localb=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:

localfunctionsay_hi()
print("Hi!")
end
localsay_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.

localmessage="Hello"
localfunctionsay_message()
print("message: "..tostring(message))
end
localsay_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.

localmessage="Hello"
localfunctionsay_message()
print("message: "..tostring(message))
end
localsay_message_clone=loadstring(string.dump(say_message))
locali=1
whiletruedo
-- see if i is a valid upvalue index
localname=debug.getupvalue(say_message,i)
ifnotnamethen
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:

localfunctionclone_function(fn)
localdumped=string.dump(fn)
localcloned=loadstring(dumped)
locali=1
whiletruedo
localname=debug.getupvalue(fn,i)
ifnotnamethen
break
end
debug.upvaluejoin(cloned,i,fn,i)
i=i+1
end
returncloned
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.

Here are some more guides tagged 'lua'
Posted January 24, 2016
Posted August 08, 2015
Posted July 04, 2015

leafo.net · Generated Tue Dec 16 16:18:13 2025 by Sitegen mastodon.social/@leafo

AltStyle によって変換されたページ (->オリジナル) /