51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

自动生成 Lua 热更新代码

游戏服务器使用 Lua 的一个重要原因是 Lua 便于热更. 即使服务器正在运行, 只需让它执行一段代码, 即可重写其中的某些函数, 达到热更新的目的. 例如模块 app 有一个函数 foo

|-----------------------|-----------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | hljs lua local M = {} function M.foo(a, b) return a + b end return M |

如果我们要将 foo 热更成将 ab 相乘, 只需要让服务器加载运行如下代码即可:

|-----------------|--------------------------------------------------------------------------------| | 1 2 3 4 | hljs lua local M = require("app") function M.foo(a, b) return a * b end |

不过很多时候, 函数并不是这么单纯. 函数常常会依赖许多上值 (upvalue), 举一个复杂点的例子:

|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | hljs lua local database = require('database') local M = {} M.n = 0 local function bar(n) return n * 2 end function M.foo(a, b) M.n = M.n + 1 return database.query(bar(a + b)) end return M |

这个例子中, 我们写热更代码时就得注意了, foo 有上值 M, databasebar. 有人说直接执行整个文件不就好了? 那可不行, Lua 很灵活, 执行整个文件很有可能出别的问题. 在这个例子中会导致 M.n 被重置 (虽然我个人不推荐在模块空间中存状态, 但是总是会有人这么做). 在一些复杂的情况下, 函数可能会有多重依赖, 比如 foo 的上值中有 bar, bar 还有它的上值等等. 这就会给热更代来很多困难.

hotfix-gen {#hotfix-gen}

为了解决这个问题, 我写了一个工具 hotfix-gen, 它能够分析代码, 提取出函数的相关依赖, 生成热更代码. 我们使用 luarocks 就能安装它:

|-----------|----------------------------------------------| | 1 | hljs bash luarocks install hotfix-gen |

我们要热更 app 模块的 foo 函数, 执行 hotfix app foo 即可:

|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | hljs bash $ hotfix app foo local database = require('database') local M = require("app") local function bar(n) return n * 2 end function M.foo(a, b) M.n = M.n + 1 return database.query(bar(a + b)) end |

这样它就能自动生成热更代码. 它会假设函数依赖的上值本身 (而非引用) 是不可变的, 例如如下的代码:

|-----------------------|-----------------------------------------------------------------| | 1 2 3 4 5 6 7 | hljs lua local n = 1 function M.foo() print(n) end n = 2 |

提取的时候就会有问题. 因此生成的代码还是需要 review 和测试的. 不过只要代码符合一定的规范, 生成的结果就没问题; 而且比起人工编写要快捷准确的多.

实现原理 {#实现原理}

hotfix-gen 的实现用的是笨办法, 也就是读取代码, 编译成语法树, 然后分析语法树. 虽然有 debug.getupvalue 可以用, 但是这必须将代码运行起来. 此外对于 local a = b * 2 这样的语句我们还需要知道 a 依赖于 b. 不过好消息是分析代码并没有那么复杂, 我们有现成的库可以用: lua-parser. lua-parser 会利用 lpeg, 将 Lua 源码解析成语法树. 我们只需要分析语法树即可.

主要工作就是识别变量的定义和引用, 这需要考虑作用域. 例如下面代码中, foo 依赖于 a 但不依赖于 b. 但如果 print(b)for 语句块外, foo 就又依赖于 b 了.

|-------------------------|-----------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 | hljs lua local a, b local function foo() for b = a, 10 do local b = 1 print(b) end -- print(b) end |

此外还必须考虑一些微妙的语法. 例如 local function f()local f = function() 是不一样的. 下面的例子中, foo 依赖于定义在它之上的 local foo = 1, 但 bar 不会, 函数 bar 中的 bar 就是它自己.

|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | hljs lua local foo = 1 local foo = function() print(foo) -- foo is 1 end local bar = 1 local function bar() bar() -- bar is itself end |

实现主要有以下几步:

  • 扫描文件 block 作用域下的每个局部变量, 并且分析它们的依赖.
  • 如果遇到目标函数, 也分析目标函数的依赖.
  • 从目标函数开始遍历依赖关系网, 得到所有需要提取的语句. 语句的顺序保持不变.
  • 生成目标代码.

最终的代码只有三百行左右, 并不很复杂. 经过测试, 能够准确处理各种情况.

赞(0)
未经允许不得转载:工具盒子 » 自动生成 Lua 热更新代码