Lua热更新机制(下)
Lua热更新机制
怎么解决热更的这些问题
场景拆分
Lua代码的实体可拆分出三类:配置、逻辑和运行时状态,其中逻辑分为常规的接口和闭包内的逻辑,运行时状态又分为全局变量和闭包内的局部变量,这里结合skynet的解决方案进行描述。
热更的核心问题
1)新老版本的差异是什么?
2)怎么处理这种差异?
解决方案的核心思路
1)为了降低热更对原有逻辑的影响,Lua热更总体采取的是原地替换的机制,即用新模块生成的table
或者function
的组件去替换已有模块的组件;
2)针对模块内值类型全局变量,以及全局表的值类型字段,对于新老版本只有一个版本有的取并集,新老版本都有的,则新版本的值覆盖老版本的值
3)针对老模块里面的闭包对象,里面的function要用新版本的对象(即逻辑更新),而upvalue则根据初始态是否为nil标识是否为运行时状态(是nil表示为运行时状态就不覆盖,否则表示常规变量直接覆盖)
4)若是闭包里面的upvalue出现了新增或者删除,则已新的为准,但是值保留
下面依次拆解源码看这个问题
一些约定
不允许热更模块修改全局变量
如下图的代码是不允许的:
local M={} function M:init() test_global() end function M:local_print() print("local function") end function M:bar() local_print() print("bar") end test_global="cccc" return M
如图所示,test_global是全局变量,模块内直接设置了全局变量的值;在现网运行过程中,可能全局变量存了一些状态,若是因为重新加载导致状态被重置无疑有着巨大风险,因此这个限制是合理的
所有外部接口必须模块内显式引入再用
代码如下:
require "module2" local M={} M.test = test_global function M:init() test_global() end function M:local_print() print("local function") end function M:bar() local_print() print("bar") end return M
如上面代码所示:test_global是我们引入的外部接口,需要显示将这个外部接口引入到table M M.test = test_global
中之后,再去使用;这个主要是为了适配Lua的加载器,仅当外部引用的时候,才会去针对不存在的接口去查全局表,才有机会转入到_ENV表的执行阶段。
对于upvalue要区分局部变量还是运行时状态
Lua提供了一种upvalue的机制,即函数可以使用外部局部变量,并且若是作为闭包对象返回之后,闭包内的逻辑+变量构成了闭包,闭包内的变量是保留的,类似C++静态变量的机制,当实际运行时,需要一种标识哪些局部变量需要重置,哪些需要保留。
构建沙箱环境
在热更时,为了避免新模块加载对运行时环境的污染,我们得先构建一个沙箱环境,防止新环境的逻辑加载影响溢出,这里通过_require来实现,如下:
local function reload_one(mod_name) ... ... local mod_ref_dummy, loader = sandbox.require(mod_name) ... ... local sandbox = (function() ... ... local get_dummy_module = (function() local dummy_module_mt local function get_or_create_obj(key) local obj = dummy_module_cache[key] if not obj then obj = setmetatable({}, dummy_module_mt) dummy_module_cache[key] = obj dummy_module_cache[obj] = key end return obj end return function(name) -- maybe 'name' contains '.' return get_or_create_obj( "[" .. name .. "]") end end)() local get_dummy_global = (function() local dummy_global_mt local function get_or_create_obj(key) local obj = dummy_global_cache[key] if not obj then obj = setmetatable({}, dummy_global_mt) dummy_global_cache[key] = obj dummy_global_cache[obj] = key end return obj end dummy_global_mt = { __metatable = "GLOBAL", __newindex = error, __pairs = error, __index = function(self, k) assert(string.isvar(k), "global field must be a valid variable name") local parent_key = dummy_global_cache[self] local key = parent_key .. "." .. k return get_or_create_obj(key) end, } return function(name) return get_or_create_obj(name) end end)() -- the base lib function never return objects out of sandbox local safe_function = { require = internal_require, -- sandbox require ... ... } local _require = (function() local make_sandbox = (function() local global_mt = { ... ... __metatable = "SANDBOX", __index = function(self, k) local v = safe_function[k] if v then return v end return get_dummy_global(k) end } return function() return setmetatable({}, global_mt) end end)() local function find_loader(mod_name) for _, searcher in ipairs(package.searchers) do local loader, absPath = searcher(mod_name) local t = type(loader) if t == "function" then return loader, absPath elseif t == "string" then table.insert(msg, loader) end end end return function(mod_name) -- load with sandbox local loader, absPath = find_loader(mod_name) local env, uv = debug.getupvalue(loader, 1) if env == "_ENV" then debug.setupvalue(loader, 1, make_sandbox()) -- every loaded module has a unique sandbox end local mod_ref_dummy = loader(mod_name, absPath) if env == "_ENV" then debug.setupvalue(loader, 1, nil) -- must set to nil, prevent for enum object end return mod_ref_dummy, loader end end)() ... ... return { require = _require, } end)()
上述代码分为三个部分:
(1)第一部分为寻找对应的Lua加载器并为模块生成闭包,在find_loader
中:
·package.searchers
里面存有不同类型模块的加载函数,针对Lua的模块,加载接口为searcher_Lua
·searcher_Lua
中通过接口findfile
在package.path
指明的路径里面寻找对应的Lua模块
·luaL_loadfilex
经过层层调用走到luaY_parser
,通过接口luaF_newLclosure
为模块创建closure
,并在mainfunc
里面将_ENV
添加到新模块的upvalue
列表中
(2)第二部分构建沙箱环境
·如上述代码所示,在reload接口里面,会先用沙箱对需要加载的模块执行沙箱内加载的操作sandbox.require()
,这里sandbox.require()
最终指向的是sandbox._require()
接口
·_require
接口里面,主逻辑流程通过debug.setupvalue
将_ENV
替换为make_sandbox
制作的沙箱:
因为从前面讨论可知:在Lua的模块代码中,整个Lua文件可以认为是一个top-level
的function
,当加载Lua文件的时候,实际是在执行一个匿名函数,只不过函数体是整个文件,那么此时执行匿名函数实际上是创建了一个Lua closure,在Lua与upvalue一文中可以知道,Lua-closure
里面有一个upvalue
列表,记录了函数内引用的外部局部变量,而若文件内引用了全局变量,这个top-level
函数也将其认为是一类upvalue
,只不过不是一一存放,而是统一将_ENV
作为自己upvalue
列表的表项,并放在第一的位置,后续引用全局变量本质上是从upvalue
列表取出_ENV
并进行访问,因此后面执行的debug.setupvalue(loader, 1, make_sandbox())
实际上是替换了_ENV
为make_sandbox
指向的table
(一个以global_mt
为metatable
的空表)。
·而make_sandbox
返回的是一个空表,这个表:
1)global_mt
是它的元表,并且实现了__index
元方法
2)所有查询这个空表找不到的对象,会先去找safe_function
,找不到则调用方法get_dummy_global
,本质上是插入了dummy_global_cache
里面建立双向引用
·通过上述两步操作,则后续要加载的Lua模块存在外部引用的时候,本地找不到默认都插入到dummy_global_cache
里面了
·此外有额外需要注意的一些点:
1)所有的外部引用都被插入到dummy_global_cache
表里面
2)外部引用无论是table、function还是其他,在沙盒内都是一个table对象,且这个table有元表为dummy_global_mt
3)dummy_global_mt
也是实现了__index
元方法的元表,当访问外部对象是table类型,且访问其一个entry,则对于entry会被以tab-name.entry_name的格式插入到dummy_global_cache
(3)第三部分执行模块加载操作
·find_loader
返回的是新创建的闭包loader
和对应文件的路径abspath
·loader
执行就是把对应lua文件进行解释和处理
除此之外,还有一个internal_require
接口需要注意:
·internal_require
解决的是所加载模块引用了其他模块的情况,即调用require
的情形
·因为前面_ENV
已经被替换了,所以有外部引用的时候,会进入global_mt
里面找,这里先找safe_function
,里面require
被重定位到internal_require
·internal_require
里面有一个_LOADED_DUMMY
记录外部引用的模块key-val pair
,若是找不到,通过接口get_dummy_table
添加到表dummy_module_cache
中
建立新老模块映射关系
构建好沙箱环境之后,为便于后续原地替换,需要先预处理一下,建立新老模块的映射图,这个是通过match_objects和match_upvalues做到的,在这之前需要先拿到新老模块的key-object的映射关系,如下所示:
local function reload_one(mod_name) ... ... -- all dummy table/function in mod_ref_dummy local dummy_path_list, obj_path_list = enum_tbl_func_dummy(mod_ref_dummy) -- find all match objects and addition objects local newTbl2oldTbl, newFunc2oldFunc = match_objects(obj_path_list, old_real_mod) -- according to the match function, find all match upvalues local newID2oldUV = match_upvalues(newFunc2oldFunc) ... ... end
这段代码分为三个部分:
·通过接口enum_tbl_func_dummy
将模块内的对象列表和外部引用列表分离开
·通过接口match_objects
将模块更新前后的新老对象进行关联
·通过接口match_values
将模块更新前后的新老upvalue
进行关联
第一部分,通过enum_tbl_func_dummy
将模块内的对象和外部引用对象拆分开,如下:
local enum_tbl_func_dummy = (function() local function iterate_function(func, depth) insert(obj_path_list, {func, table.unpack(path)}) for idx, name, value in debug.upvalues(func) do local t = type(value) -- type of value if t == "function" or t == "table" then path[depth] = name iterate_object(value, t, depth) path[depth] = nil end end end local function iterate_table(tbl, depth) if is_dummy(tbl) then insert(dummy_path_list, {tbl, table.unpack(path)}) else insert(obj_path_list, {tbl, table.unpack(path)}) for key, value in pairs(tbl) do local t = type(value) -- type of value if t == "function" or t == "table" then path[depth] = key iterate_object(value, t, depth) path[depth] = nil end end end end function iterate_object(obj, t, depth) if has_traversed[obj] then return end has_traversed[obj] = true depth = depth + 1 if t == "function" then return iterate_function(obj, depth) else -- table return iterate_table(obj, depth) end end return function(object) local t = type(object) if t ~= "function" and t ~= "table" then return end iterate_object(object, t, 0) local dummy, obj = dummy_path_list, obj_path_list return dummy, obj end end)()
·mod_ref_dummy
是sandbox.require
返回的以所加载的文件为函数体的闭包
·enum_tbl_func_dummy
中,根据对象类型进行处理,发现是function
类型,调用接口iterate_function
,否则是table类型,调用iterate_table
,对于所加载的模块来说,可以是top-level
的function,此时下面调用的是iterate_function
,并且:
has_traversed
这个table,里面记录了这个闭包里面哪些object已经被遍历,避免无限嵌套
·iterate_function
中,先把自己添加到obj_path_list
这个数组中,然后遍历这个func
的upvalue
列表
因为这个文件内定义的function
一定是本文件的,所以必然归入到obj_path_list
列表中
·iterate_table
中:
对于所加载模块,若是存在外部引用,那么外部引用是一个以global_mt
或者dummy_module_mt
为元表的table,所以在iterate_table
中,会先看是不是外部引用is_dummy
是的话丢到dummy_path_list
,否则丢到obj_path_list
第二部分,通过match_objects
实现新老模块同名对象的关联:
local function reload_one(mod_name) ... ... -- all dummy table/function in mod_ref_dummy local dummy_path_list, obj_path_list = enum_tbl_func_dummy(mod_ref_dummy) -- find all match objects and addition objects local newTbl2oldTbl, newFunc2oldFunc = match_objects(obj_path_list, old_real_mod) ... ... end local function match_objects(TF_path_list, old_real_mod) for _, TF_path in ipairs(TF_path_list) do local newTF = TF_path[1] -- table/function from 'the reload one' local ok, old_one = pcall(find_oldTF, old_real_mod, TF_path) if not ok then type_error(TF_path, "type mismatch: ") end local t = type(newTF) local newTF2oldTF = t == "table" and newTbl2oldTbl or newFunc2oldFunc local oldTF = newTF2oldTF[newTF] newTF2oldTF[newTF] = old_one end return newTbl2oldTbl, newFunc2oldFunc -- , additionTF end
对于match_objects
的参数:
·old_real_mod
,对于使用这个库进行热更的模块,最终模块需要return M
这样一个语句,用于require
返回一个table
,old_real_mod
用于接收这个table,里面键值对是模块内定义的function
和var
及其值
·但是对于Lua的require
来说,若是这个模块事先已经加载了,则require(mod-name)
返回的是已加载的table(即require
会先找本地的package.loaded
表,有的话就直接返回),所以这里是old_real_mod
·TF_path_list
接收的是前面obj_path_list的值,obj_path_list是一个列表,记录的是模块内所有定义的function、table等的名字到对象地址的映射关系,其中第一个元素即(obj_path_list[1]存放的是所加载模块的地址(在Lua中,一个模块可以认为是一个表)),如下图:
在match_objects
中:
1)for _, TF_path in ipairs(TF_path_list) do
主循环用于遍历更新后模块的每个item,对里面的每个item,通过接口find_oldTF
,去old_real_mod
里面找到同名对象的地址。
对于TF_path_list来说,TF_path_list里面除了第一个,TF_path_list[2 - len]每一项是一个数组,含有两个元素,第一个是对象地址,第二项是对象的名字
2)match_objects
做了限制assert(type(newTF)==type(old_one))
,即不允许更新后模块类型发生改变。
例如,更新前,bar是function类型,更新后是integer类型,这种是不允许的
3)匹配的结果分别丢到newTbl2oldTbl
和newFunc2oldFunc
里面,建立新函数(表)到老函数(表)addr->addr的映射
4)find_oldTF
处理的node有两种,table
类型,直接处理;function
类型,则遍历其upvalue
逐个进行处理
第三部分,通过match_upvalues
实现新老模块同名upvalue的关联:
local function match_upvalues(newFunc2oldFunc) -- find match func's upvalues local newID2oldUV = {} for newFunc, oldFunc in pairs(newFunc2oldFunc) do for idx, name, value in debug.upvalues(newFunc) do if name == "" then break end local _, old_idx = find_upvalue(oldFunc, name) if old_idx then local id = debug.upvalueid(newFunc, idx) local old_id = debug.upvalueid(oldFunc, old_idx) local oldUV = newID2oldUV[id] if not oldUV then newID2oldUV[id] = { old_func = oldFunc, old_idx = old_idx, old_id = old_id } else assert(old_id == oldUV.old_id, string.format("Ambiguity upvalue: %s .%s", tostring(newFunc), name)) end end end end return newID2oldUV end
将新老模块里面同名function进行映射之后,match_upvalues
解决的是这些同名函数内upvalue
的映射问题,这里主体框架如下:
·for newFunc, oldFunc in pairs(newFunc2oldFunc)
,newFunc2oldFunc
里面存放的是新老同名方法的映射对,这里逐个取出进行处理;
·for idx, name, value in debug.upvalues(newFunc)
,debug.upvalues(newFunc)
里面存放的是更新后方法里面upvalue
的列表,逐个取出,并根据名字,通过接口find_upvalue
在老方法里面去找,找到的话把映射关系存入newID2oldUV
里面;
·值得注意的是debug.upvalueid
里面标识每个upvalueid
用的是对应upvalue
在内存中的地址,所以这个id是全局唯一的。
迭代更新
完成同名模块新老版本的映射关系之后,需要在原地将老模块的指令、值等替换成预期要更新的新模块,这属于迭代更新要处理的逻辑范畴,这分为两步,第一步是将函数里面的upvalue进行更新(patch_funcs_upvalue
来处理),第二步是将表里面字段进行更新(由patch_tables
处理):
local function reload_one(mod_name) ... ... -- first: make new function join to the old upvalue, and update the upvalue's value patch_funcs_upvalue(newID2oldUV, newFunc2oldFunc) -- must before patch table -- second: update new table's value to the old table, contains the mod_ref_dummy itself and old_real_mod itself patch_tables(newTbl2oldTbl) ... ... end
upvalue迭代更新
func里面upvalue迭代更新走的是接口patch_funcs_upvalue
,如下代码列表所示:
local function patch_funcs_upvalue(newID2oldUV, newFunc2oldFunc) for newFunc in pairs(newFunc2oldFunc) do for idx, name, value in debug.upvalues(newFunc) do if name == "" then break end local id = debug.upvalueid(newFunc, idx) local oldUV = newID2oldUV[id] if oldUV then debug.upvaluejoin(newFunc, idx, oldUV.old_func, oldUV.old_idx) if value ~= nil and type(value) ~= "table" then debug.setupvalue(newFunc, idx, value) end end end end end
对于这个接口,我们这么理解:
·newFunc2oldFunc
是通过match_objects
获取的,里面存放的是新老function的映射关系
·newID2oldUV
是通过match_upvalues
获取的,里面存放的是同function新老upvalue的映射关系
·patch_funcs_upvalue
则通过遍历newFunc
的upvalue
,逐个去newID2oldUV
里面找,通过接口debug.upvaluejoin
关联起来
·match_objects
实际上把模块内的对象都找出来放在表里面了,包括闭包对象,而闭包对象本身一旦创建后就是一块独立的内存,其变量和逻辑不因模块发生更新而变更,这意味着对于相同function返回的闭包,内存中可能因模块更新存在两套代码和变量,而patch_funcs_upvalue
的目的就是将这两套闭包模型的upvalue
指向同一块内存
upvalue分为四种情况:
1)新模块有老模块没有的upvalue,直接加入即可
2)新模块没有老模块有的upvalue,不用管,自然废弃
3)新老模块都有,存放的是运行时状态的upvalue,在这里以初始值是否为nil进行区分,针对此类upvalue不能动,需要保留其运行时状态
4)新老模块都有,存放的是临时状态的upvalue,重置其初始值
如下图所示:

test模块中,function bar含有三个upvalue,其中a、b、c是已有的,则进行关联的,对于已经废弃的e和新增的d则不做处理。
table的迭代更新
table也遵循原地更新的原则,它的值的处理分为四种情况:
1)table中新增的字段:即更新前没有的字段&更新后有的字段,这种对应于分支oldTbl[key] == nil的情况,这种把新的字段加到老表即可
2)table中删除的字段:即更新前有,更新后没有的字段,这种因为后续逻辑会更新,就不用管了
3)table中发生变化的字段:更新前后都有的字段,对应于分支 type(value) ~= "table",这种直接替换为新的值
4)table中涉及外部引用部分:对应于分支getmetatable(value) ~= nil
,因为有可能外部引用也发生变化,在这里统一替换为新表的dummy object
在后面流程统一替换
local function patch_tables(newTbl2oldTbl) for newTbl, oldTbl in pairs(newTbl2oldTbl) do for key, value in pairs(newTbl) do if type(value) ~= "table" or -- copy values not a table getmetatable(value) ~= nil or -- copy dummy oldTbl[key] == nil then -- copy new object oldTbl[key] = value end end end end
上面的逻辑需要我们注意:
要想使用这个热更功能,我们模块内的table只读不写,也就是说只能存一些配置信息,但是不能存运行时状态,否则会在热更的时候被覆盖。
但也有一些问题:
新表的value为table类型怎么处理?
外部引用替换
通过上面的步骤,我们已经实现新老模块同名字段的关联,并且实现table和upvalue的替换,但是新老表的外部引用还是被用dummy object替代的状态,在后续更新之前,我们先把dummy object替换为真正的外部对象的地址,如下
local function reload_one(mod_name) ... ... if loader then -- update '_ENV' for all closure created by the loader debug.setupvalue(loader, 1, _ENV) end -- replace all dummies with real value solve_dummies(dummy_path_list, old_real_mod) ... ... end local function solve_dummies(dummy_path_list, old_real_mod) for _, dummy_path in pairs(dummy_path_list) do local dummy = dummy_path[1] local real_value, key = sandbox.value(dummy) set_object(real_value, old_real_mod, dummy_path) end end local _value = (function() local function get_G(dummy) -- will get the global value for that dummy object (maybe any value type) local k = dummy_global_cache[dummy] --key: a.b.c local G = _G -- %a means all alphabet, contains upper and lower -- %w means all alphabet and digital for w in string.gmatch(k, "[_%a][_%w]*") do G = G[w] end return G, k end local function get_M(dummy) local k = dummy_module_cache[dummy] local M = debug.getregistry()._LOADED local from, to, name = string.find(k, "^%[([_%a][_.%w]*)%]") local mod = assert(M[name]) for w in string.gmatch(k:sub(to+1), "[_%a][_%w]*") do if mod == nil then return nil end mod = mod[w] end return mod, k end return function(dummy) local meta = getmetatable(dummy) if meta == "GLOBAL" then return get_G(dummy) elseif meta == "MODULE" then return get_M(dummy) end end end)() local function set_object(real_value, old_real_mod, TFD_path) local node = old_real_mod local len = #TFD_path for idx = 2, len - 1, 1 do local key = TFD_path[idx] local t = type(node) if t == "table" then node = node[key] elseif t == "function" then node = find_upvalue(node, key) end end local t = type(node) if t == "table" then node[TFD_path[len]] = real_value elseif t == "function" then local _, i = find_upvalue(node, TFD_path[len]) debug.setupvalue(node, i, real_value) end end
·dummy_path_list
由enum_tbl_func_dummy
生成的外部引用的路径列表,路径格式为“a.b.c”
·string.gmatch(s,pattern)
返回一个迭代器函数,每次调用此函数,会返回一个在字符串s找到下一个符合pattern
描述的子串
·执行sandbox.value(dummy)
的时候,去_value
里面取,因为是外部引用,调用接口get_G
,从全局表里面取到实际外部引用的地址_G[w]
·而在slove_dummies
中,会先从dummy_path_list
里面逐个对象取值,其中dummy_path
首项是对象在global_mt
的地址,dummy_path[1]
取的是对象的地址,通过_value
中dummy_global_cache
反向取到对象的key
,然后从_G
中取到,返回地址和key
·set_object
通过此接口,将地址替换为全局表中的地址
而在set_object
中:
set_object(real_value, old_real_mod, dummy_path) local function set_object(real_value, old_real_mod, TFD_path) local node = old_real_mod local len = #TFD_path for idx = 2, len - 1, 1 do local key = TFD_path[idx] local t = type(node) if t == "table" then node = node[key] elseif t == "function" then node = find_upvalue(node, key) end end local t = type(node) if t == "table" then node[TFD_path[len]] = real_value elseif t == "function" then local _, i = find_upvalue(node, TFD_path[len]) debug.setupvalue(node, i, real_value) end end local function find_upvalue(func, upvalue_name) for idx, name, value in debug.upvalues(func) do if name == "" then return end if name == upvalue_name then return value, idx end end end
set_object
中
1)第一个for循环,通过node=node[key]
递归查询,找到终端的引用key
2)然后通过node[TFD_path[len]]
将所引用的item的val
替换为real_value
·引用的是table
的话,就直接设置
·引用的是function
的话,就通过setupvalue
进行设置
运行时环境替换
分为以下几类:
·replace_recursive_table(mt)
:基础类型元表替换
·replace_stack_frame
:栈元素替换
·replace_recursive_table(debug.getregistry())
:注册表元素替换
下面逐个进行分析。
基础类型元表更新
local function replace_all_function(newFunc2oldFunc) local oldF2newF = {} for newFunc, oldFunc in pairs(newFunc2oldFunc) do oldF2newF[oldFunc] = newFunc end local co = coroutine.running() local exclude = {[co] = true} local function replace_recursive_function(func) if exclude[func] then return end exclude[func] = true for idx, name, value in upvalues(func) do if value then local t = type(value) if t == "function" then local newOne = oldF2newF[value] if newOne then setupvalue(func, idx, newOne) else replace_recursive_function(value) end else replace_recursive(value, t) end end end end local function replace_recursive_table(tbl) if exclude[tbl] then return end exclude[tbl] = true local oldKey2newKey for key, value in next, tbl do local t_key = type(key) if t_key == "function" then local new_key = oldF2newF[key] if new_key then if not oldKey2newKey then oldKey2newKey = {} end oldKey2newKey[key] = new_key else replace_recursive_function(key) end else replace_recursive(key, t_key) end local t_value = type(value) if t_value == "function" then local new_value = oldF2newF[value] if new_value then rawset(tbl, key, new_value) else replace_recursive_function(value) end else replace_recursive(value, t_value) end end if oldKey2newKey then for oldKey, newKey in pairs(oldKey2newKey) do local value = tbl[oldKey] rawset(tbl, oldKey, nil) rawset(tbl, newKey, value) end end local mt = getmetatable(tbl) if mt then return replace_recursive_table(mt) end end local function replace_recursive_userdata(ud) if exclude[ud] then return end exclude[ud] = true local uv = getuservalue(root) if uv then local t = type(uv) if t == "function" then local nv = oldF2newF[uv] if nv then setuservalue(ud, nv) else replace_recursive_function(uv) end else replace_recursive(uv, t) end end local mt = getmetatable(ud) if mt then return replace_recursive_table(mt) end end function replace_recursive(value, t) local replace_func = replace_by_type[t] if replace_func then replace_func(value) end end -- nil, number, boolean, string, thread, function, lightuserdata may have metatable for _,v in pairs { nil, 0, true, "", co, replace_recursive, debug.upvalueid(replace_recursive,1) } do local mt = getmetatable(v) if mt then replace_recursive_table(mt) end end ... ... end
1)replace_all_function
:第一个for循环,oldF2newF
新老func地址的映射关系
2)replace_recursive_table
:用于处理表格内引用模块内地址的替换
3)for key, val in next, tbl do .. end
:tbl
中逐个key-val进行处理
4)type(key) == "function"
:表示key为function类型,到oldF2newF里面查:
·查到的话,将新老key的映射关系插入到oldKey2newKey
里面
·没查到,表示是一个新模块删除的function,则不不用插入映射表,但是需要递归处理老function里面的元素replace_recursive_function
5)type(value)=="function"
,查表oldF2newF
·查到的话,直接把key对应的val设置为新的值rawset(tbl, key, new_value)
·没查到,就直接处理函数里面的东西replace_recursive_function
6)for oldKey, newKey in pairs(oldKey2newKey)
,逐个KV进行处理,把老的key删除,插入新的KV pair
7)if mt then return replace_recursive_table(mt) end
:若此表格存在元表,则对元表进行相同的replace_recursive_table
操作
运行时函数更新
local function replace_recursive_function(func) if exclude[func] then return end exclude[func] = true for idx, name, value in upvalues(func) do if value then local t = type(value) if t == "function" then local newOne = oldF2newF[value] if newOne then setupvalue(func, idx, newOne) else replace_recursive_function(value) end else replace_recursive(value, t) end end end end
replace_recursive_function用于替换运行时闭包的逻辑和upvalue,其中
1)对于闭包来说,其类型是function类型
2)当闭包函数里面,调用其他函数的时候,其他函数也是这个函数的一个upvalue
3)所以在遍历upvalue的时候,发现对象是function类型,则通过setupvalue将老的upvalue关联到新的接口上(值类型怎么办?)
4)也有可能是删除了代码,即老的模块调用了接口func1,但是新的接口删除了func1,逻辑也删除了,此时不用管,递归处理下面的调用关系(这种意味着,若是闭包发生更新,老闭包调用了一个接口,这个接口在更新后被删除了,老闭包的这块逻辑还是保留的,并且继续处理下层已经被替换的接口)
上述3)和4)意味着replace_recursive_function
解决运行时环境中,闭包内接口替换的问题。
5)其他的调用接口replace_recursive进行处理
那么这里有个问题,函数内的指令怎么更新呢?
以下面test.lua为例:
local M={} function M:bar() end function M:foo() end return M
当test.lua被处理完之后,其实会放在一个表里面,里面有bar和foo两个项,在处理虚拟机内存中M模块的实例时候,就把M的实例里面的表项遍历一遍了,发现bar的话,就替换为新的函数地址;即完成更新,而replace_recursive_function实际做的是bar里面,涉及到其他函数调用的地址更新。
local function replace_stack_frame(co, level) local info = getinfo(co, level, "f") if not info then return end local f = info.func info = nil replace_recursive_function(f) for idx, name, value in locals(co, level) do if value then local t = type(value) if t == "function" then local newOne = oldF2newF[value] if newOne then setlocal(co, level, idx, newOne) else replace_recursive_function(value) end else replace_recursive(value, t) end end end return replace_stack_frame(co, level + 1) end
一些基本知识:
1)debug.getinfo(level,arg)
用于获取函数调用的基本信息,返回一个包含函数信息的table
,其中:
·level
表示函数调用的层级,0:getinfo自身,1:调用getinfo的函数以此类推
·arg
:决定table里面包含的信息,f
表示是函数信息,值在table.func
里面
2)debug.setlocal([thread,]level,local,value)用于设置指定thread
第level层下标为local
的局部变量的值设置为value
·debug.getlocal([thread,]f[,what])
用于返回在栈f层处,索引为local的局部变量的名字和值
3)在replace_stack_frame
中
①replace_recursive_function
②for idx, name, value in locals(co, level)
轮询指定线程co
中指定level
层级的局部变量,逐个进行处理
·type(value)==function
的话,通过setloc
al
进行修改
·其他类型,通过replace_recursive
进行处理
③处理下一层级replace_stack_frame(co, level + 1)
参考资料
https://john.js.org/2020/10/27/Lua-Runtime-Hotfix/
https://blog.51cto.com/bosswanghai/1832133
https://blog.csdn.net/Yueya_Shanhua/article/details/52241544
https://github.com/zhyingkun/lua-5.3.5/blob/master/