изменить/обновить значение локальной переменной (Lua upvalue)

Я написал скрипт для горячей перезагрузки уже requireed модулей. Однако это работает лишь частично...

Мой подход к этой задаче довольно прост. Я изменил функцию Lua require, чтобы она запоминала загруженные модули вместе с меткой времени и путем к файлу. Затем я использую сценарий оболочки, чтобы наблюдать за временем модификации этих файлов и повторно запрашивать их, если они изменились. Я просто dofile() и, если ошибок не происходит, я беру возвращаемое значение и (повторно) присваиваю его package.loaded[<module>]. Все идет нормально.

Все это отлично работает, когда я использую глобальные переменные, например. foo = require "foobar", но когда я использую локальные назначения, такие как local foo = require "foobar", моя горячая замена выходит из строя (частично)!

Кажется, что пакет выгружается, как и предполагалось, однако локальная переменная (из приведенного выше назначения) по-прежнему содержит старую ссылку или старое значение, полученное при первом вызове require.

Моя идея состояла в том, чтобы использовать функции Lua debug.getlocal и debug.setlocal для поиска всех локальных переменных (значений вверх в стеке) и обновления их значений/ссылок.

НО я получаю сообщение об ошибке, что значение, которое я хочу изменить, находится "вне диапазона"... Может ли кто-нибудь помочь мне, пожалуйста? Что мне делать или как я могу обойти это?

Полный код находится на Gist, однако важные/уместные фрагменты...

  1. функция local_upvalues() в строке 27, которая собирает все доступные значения upvalue
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. и debug.setlocal() в строке 89, которая пытается обновить upvalue, содержащее ссылку на устаревший модуль.
        -- 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

person herrsch    schedule 26.12.2019    source источник


Ответы (2)


Вы можете использовать метатаблицу с __index. Вместо возврата package.loaded[resource] или _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
  })

И

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
  })

Делая это, вам вообще не нужно искать upvalues, так как это заставит любые local требовать результаты всегда ссылаться на актуальный ресурс.

Вероятны случаи, когда это не будет работать должным образом или иным образом сломает модуль, но с некоторой настройкой это возможно.

person Nifim    schedule 27.12.2019
comment
Я должен попробовать... В конце концов, это хороший вариант. Единственный случай, когда я вижу эту ошибку, - это модуль классов, который я использую (наследование через метатаблицы), который также использует ключ __index ... но я рассмотрю это, поскольку это будет дешевле, чем повторная ссылка на все локальные и глобальные переменные. Спасибо. - person herrsch; 27.12.2019

Я принял ответ @Nifim. Однако, насколько я могу судить, это будет работать только для таблиц. Но require также может возвращать значение любого типа. - Тем не менее, это хорошее решение, которое может работать с некоторыми настройками...

Однако, просто для справки - у меня тоже работает мой подход! Во-первых, я удалил оболочку pcall() из debug.getlocal(), потому что это вводило другой уровень стека и, таким образом, возвращало неверные значения потоков и индексов, которые не работали с debug.setlocal(). Наконец, я переместил вызов debug.setlocal в ту же функцию (= ту же область), чтобы проверить и переназначить за один шаг!

См. мой код функции rereference(absolete, new) ниже.

local thread = 1
while debug.getinfo(thread) ~= nil do
    local index, name, value = 0, nil, nil
    repeat
        index = index + 1
        name, value = debug.getlocal(thread, index)
        if name ~= nil
        and name ~= "absolete"
        and name ~= "new"
        then
            if value == absolete then
                if debug.setlocal(thread, index, new) == name then
                    print(string.format(
                        "%s local upvalue '%s' has been re-referenced",
                        os.date("%d.%m.%Y %H:%M:%S"),
                        name
                    ))
                end
            end
        end
    until name == nil
    thread = thread + 1
end
person herrsch    schedule 27.12.2019