Lua热更新机制(下)

ronald6个月前职场3640

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通过接口findfilepackage.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-levelfunction,当加载Lua文件的时候,实际是在执行一个匿名函数,只不过函数体是整个文件,那么此时执行匿名函数实际上是创建了一个Lua closure,在Lua与upvalue一文中可以知道,Lua-closure里面有一个upvalue列表,记录了函数内引用的外部局部变量,而若文件内引用了全局变量,这个top-level函数也将其认为是一类upvalue,只不过不是一一存放,而是统一将_ENV作为自己upvalue列表的表项,并放在第一的位置,后续引用全局变量本质上是从upvalue列表取出_ENV并进行访问,因此后面执行的debug.setupvalue(loader, 1, make_sandbox())实际上是替换了_ENVmake_sandbox指向的table(一个以global_mtmetatable的空表)。

    ·而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_dummysandbox.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这个数组中,然后遍历这个funcupvalue列表

    因为这个文件内定义的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返回一个tableold_real_mod用于接收这个table,里面键值对是模块内定义的functionvar及其值

    ·但是对于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中,一个模块可以认为是一个表)),如下图:

image-20220122110424880.png

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)匹配的结果分别丢到newTbl2oldTblnewFunc2oldFunc里面,建立新函数(表)到老函数(表)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则通过遍历newFuncupvalue,逐个去newID2oldUV里面找,通过接口debug.upvaluejoin关联起来

    ·match_objects实际上把模块内的对象都找出来放在表里面了,包括闭包对象,而闭包对象本身一旦创建后就是一块独立的内存,其变量和逻辑不因模块发生更新而变更,这意味着对于相同function返回的闭包,内存中可能因模块更新存在两套代码和变量,而patch_funcs_upvalue的目的就是将这两套闭包模型的upvalue指向同一块内存

upvalue分为四种情况:

    1)新模块有老模块没有的upvalue,直接加入即可

    2)新模块没有老模块有的upvalue,不用管,自然废弃

    3)新老模块都有,存放的是运行时状态的upvalue,在这里以初始值是否为nil进行区分,针对此类upvalue不能动,需要保留其运行时状态

    4)新老模块都有,存放的是临时状态的upvalue,重置其初始值

如下图所示:

image-20220122115826342.png

    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_listenum_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]取的是对象的地址,通过_valuedummy_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 .. endtbl逐个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话,通过setlocal进行修改

    ·其他类型,通过replace_recursive进行处理

    ③处理下一层级replace_stack_frame(co, level + 1)


参考资料

    https://john.js.org/2020/10/27/Lua-Runtime-Hotfix/

    云风的 BLOG: 如何让 lua 做尽量正确的热更新 (codingnow.com)

    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/


相关文章

Lua的Upvalue和闭包(二)

Lua的Upvalue和闭包(二)

Lua闭包和Upvalue的实现    前面文章介绍了Lua闭包和upvalue的概念,本文简单过一下Lua对于闭包和upvalue的实现以加深理解。Lua闭包结构    Lua在内存的结构如下所示:#define ClosureHeader \ CommonHeader; lu_by...

Lua和so

Lua和so

    实际在应用开发过程中,会用到很多第三方的库;Lua由于其易嵌入的特性,不仅可以使用Lua编写的库,也可以将C++的库进行二次封装供Lua调用。这里我们以实现在Lua中解析xml文件格式场景,结合C++编写的“tinyxml2” 这个库为例进行讲解:库的准备及编译第一步,下载“tinyxml2”的源码下载链接:leethomason/tinyxml2:...

LUA数据结构(三)

Lua数据结构userdata    Lua官方的介绍:userdata是一种用户自定义数据,用于表示一种由应用程序或者C/C++语言库创建的类型,可以将任意C/C++类型的数据(通常是struct、指针)存储到Lua变量中调用。    在实际应用过程中,C/C++接口调用LuaL_newuserdata就会分配指定大...

Lua热更新机制(上)

Lua热更新机制一个Lua热更新demo    Lua在游戏开发中能广泛使用不仅由于其轻量易嵌入的特性,还有一个重要的点是易于热更新,设想在产品线上运营过程中,出现bug需要修复,频繁停机对于产品体验影响大,也影响口碑;所以实际运营我们是希望能尽量避免停止服务进行代码更新的操作,下面先从一段比较简单的代码看Lua的热更新机制:require &qu...

LUA数据结构(二)

LUA数据结构(二)

Lua数据结构thread    Lua中,最主要的线程是协程,它和线程差不多,拥有独立的栈、局部变量和指令指针;和线程区别在于线程可以同时运行多个,而协程同一时刻只能有一个运行    Lua协程接口都放在table coroutine里面,主要包括以下几个:coroutine.create,创建一个协程corouti...

关于LUA(上)

    Lua是一种轻量小巧的脚本语言,C语言编写,并提供了易于使用的扩展接口和机制,易于嵌入到应用中,在游戏开发中经常被用来进行外层业务系统的开发。Lua的table    Lua的基本数据类型有八种,分别是:nil、boolean、number、string、userdata、function、thread 和 t...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。