Search code examples
luahot-reloadupvalue

change/update value of a local variable (Lua upvalue)


I've written a script to hot-reload already requireed modules. It work s only partially however...

My approach to this task is quite simple. I changed Lua's require function so that it remembers modules that it loaded together with a timestamp and its file path. Then I use a shell script to observe the modification time of those files and re-require them if they changed. I simply dofile() and if no errors happen, I take the return value and (re-)assign it at package.loaded[<module>]. So far so good.

All of this works perfect when I use global variables, e.g. foo = require "foobar", but when I use local assignments, like local foo = require "foobar", my hotswapper failes (partially)!

It seems that the package gets swapped out like intended, however the local variable (from the assignment above) still holds an old reference or the old value that it got when require was called the first time.

My idea was to use Lua's debug.getlocal and debug.setlocal functions to find all local variables (upvalues in stack) and update their values/references.

BUT I get an error that the upvalue I want to change is "out of range"... Could somebody help me please? What should I do or how could I work around this?

The complete code is over at Gist, the important/relevant snippets however are...

  1. the function local_upvalues() at line 27, which collects all available upvalues
local function local_upvalues()
    local upvalues = {}
    local failures = 0
    local thread = 0
    while true do
        thread = thread + 1
        local index = 0
        while true do
            index = index + 1
            local success, name, value = pcall(debug.getlocal, thread, index)
            if success and name ~= nil then
                table.insert(upvalues, {
                    name = name,
                    value = value,
                    thread = thread,
                    index = index
                })
            else
                if index == 1 then failures = failures + 1 end
                break
            end
        end
        if failures > 1 then break end
    end
    return upvalues
end
  1. and the debug.setlocal() at line 89, which tries to update the upvalue that holds the absolete module reference
        -- update module references of local upvalues
        for count, upvalue in ipairs(local_upvalues()) do
            if upvalue.value == package.loaded[resource] then
                -- print(upvalue.name, "updated from", upvalue.value, "to", message)
                table.foreach(debug.getinfo(1), print)
                print(upvalue.name, upvalue.thread, upvalue.index)
                debug.setlocal(upvalue.thread, upvalue.index, message)
            end
        end
        package.loaded[resource] = message -- update the absolete module

Solution

  • You can use a metatable with __index. Rather then returning package.loaded[resource] or _require(resource) return:

    _require(resource)
    return setmetatable({}, --create a dummy table
      {
        __index = function(_, k)
          return package.loaded[resource][k] -- pass all index requests to the real resource.
        end
      })
    

    And

    package.loaded[resource] = message -- update the absolete module
    
    print(string.format("%s %s hot-swap of module '%s'",
        os.date("%d.%m.%Y %H:%M:%S"),
        stateless and "stateless" or "stateful",
        hotswap.registry[resource].url
      ))
    return setmetatable({}, 
      {
        __index = function(_, k)
          return package.loaded[resource][k]
        end
      })
    

    Doing this you shouldn't need to look up the upvalues at all, as this will force any local require results to always reference the up-to-date resource.

    There are likely cases where this will not work well, or otherwise break a module, but with some tweaking it can.