One of the main reasons why game servers use Lua is that it is easy to hot update. Even if the server is running, you can rewrite some of its functions for hot update purposes by simply having it execute a piece of code. For example, the module app has a function foo

1
2
3
4
5
6
7
local M = {}

function M.foo(a, b)
    return a + b
end

return M

If we want to hot change foo to multiply a and b, we just need to have the server load and run the following code:

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

In many cases, however, functions are not so simple. Functions often depend on a number of upvalues, for a more complex example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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

In this example, we have to be careful when writing hotfix code, foo has upper values M , database and bar . Some people say it’s better to just execute the whole file? No, Lua is flexible, and executing the whole file may cause other problems. In this case it would cause M.n to be reset (I personally don’t recommend storing state in module space, but people do it all the time). In some complex cases, functions may have multiple dependencies, e.g. bar in the upper value of foo, bar in its upper value, etc. This can make hot-replacement a lot more difficult.

hotfix-gen

To solve this problem, I wrote a tool hotfix-gen that analyzes the code, extracts the dependencies of the functions, and generates the hotfix code. We can install it using luarocks:

1
luarocks install hotfix-gen

We want to hotfix the app module’s foo function, by running hotfix app foo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ 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

This allows it to automatically generate hotfix code. It assumes that the upper value itself (not the reference) on which the function depends is immutable, as in the following code:

1
2
3
4
5
6
7
local n = 1

function M.foo()
    print(n)
end

n = 2

The extraction will be problematic. So the generated code still needs to be reviewed and tested. However, as long as the code meets certain specifications, the generated results will be fine; and it is much faster and more accurate than writing it manually.

Implementation principle

The hotfix-gen implementation uses a dumb approach, which is to read the code, compile it into a syntax tree, and then analyze the syntax tree. There is a debug.getupvalue, but this must be run with the code. In addition, for statements like local a = b * 2 we need to know that a depends on b. But the good news is that parsing the code is not that complicated, we have a ready-made library: lua-parser. The lua-parser will use lpeg to parse the Lua source code into a syntax tree. We just need to parse the syntax tree.

The main task is to identify variable definitions and references, which requires consideration of scope. For example, in the following code, foo depends on a but not on b . But if print(b) is outside the for block, then foo will depend on b again.

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

There are also some subtle syntaxes that must be considered. For example, local function f() is not the same as local f = function(). In the example below, foo depends on the local foo = 1 defined on top of it, but bar does not, the bar in the function bar is itself.

1
2
3
4
5
6
7
8
9
local foo = 1
local foo = function()
    print(foo) -- foo is 1
end

local bar = 1
local function bar()
    bar() -- bar is itself
end

The implementation consists of the following steps:

  • Scan each local variable in the block scope of the file, and analyze their dependencies.
  • If a target function is encountered, analyze the target function’s dependencies as well.
  • Iterate through the dependency network starting from the target function to get all the statements to be extracted. The order of the statements remains the same.
  • Generate the target code.

The final code is only about 300 lines, which is not very complicated. After testing, it can handle various cases accurately.