7

我们正在使用向 Lua 公开图形 API 的 Love2d Lua 游戏引擎。我们正在尝试序列化一个包含游戏世界所有保存游戏数据的巨型哈希表。这个散列包括一些函数,其中一些函数调用 Love2d C 函数。

为了序列化散列中的函数,我们使用 string.dump,并使用 loadstring 将它们重新加载。这适用于纯 Lua 函数,但是当我们尝试序列化然后加载回调用包装 C 函数的函数(例如 Love2d api 中的函数)时,loadstring 返回 nil。

考虑以下通过 Love2d 的图形引擎在屏幕上绘制“hello, world”的简单程序:

function love.load()
    draw = function()
        love.graphics.print('hello, world', 10, 10)
    end
end
function love.draw()
    draw()
end

我们希望能够做到这一点:

function love.load()
    draw_before_serialize = function()
        love.graphics.print('hello, world', 10, 10)
    end

    out = io.open("serialized.lua", "wb")
    out:write('draw = load([[' .. string.dump(draw_before_serialize) .. ']])')
    out:close()

    require "serialized"
end
function love.draw()
    draw()
end

这样做会写入磁盘上的 Lua 文件,该文件包含未编译的 Lua 和 Lua 字节码的混合,如下所示:

draw = load([[^[LJ^A^@      
       @main.lua2^@^@^B^@^B^@^D^E^B^B4^@^@^@%^A^A^@>^@^B^AG^@^A^@^Qhello, world 
       print^A^A^A^B^@^@]])

此方法适用于不调用 C 模块的 Lua 函数。我们认为这是问题所在,因为此示例确实有效:

function love.load()
    draw_before_serialize = function()
        print('hello, world')
    end

    out = io.open("serialized.lua", "wb")
    out:write('draw = load([[' .. string.dump(draw_before_serialize) .. ']])')
    out:close()

    require "serialized"
end
function love.draw()
    draw()
end

它不调用 Love2d 图形方法,而是打印到控制台。

经过更多的测试,我们困惑地发现这个例子确实有效:

function love.load()
    draw_before_serialize = function()
        love.graphics.print('hello, world', 10, 10)
    end

    draw = load(string.dump(draw_before_serialize))
end
function love.draw()
    draw()
end

在这里,我们实际上并没有将函数写到磁盘上,而是直接转储它,然后立即将其加载回来。"wb"我们认为罪魁祸首可能是没有设置二进制写入模式标志(

有任何想法吗?

4

1 回答 1

6

我认为问题在于字符串的格式。Nicol Bolas 对围绕字节码转储的 [[]] 引号可能是正确的,但这指出了一个更大的问题;字节码实际上可以是任何东西,但您将其视为可以写入和读取文本文件的普通字符串。您的上一个演示演示了此问题,您在其中加载转储的字符串而无需将其写入文件。

我认为,包含函数的表的序列化程序的这种实现可以满足您的需求,但我也认为它已损坏(好吧,无论如何我都无法使其正常工作……)。无论如何,它在正确的轨道上。您需要格式化字节码,然后将其写入文件。

我敢肯定有更好的方法来做到这一点,但这有效:

1.    binary = string.dump(some_function)
2.    formatted_binary = ""
3.    for i = 1, string.len(binary) do
4.        dec, _ = ("\\%3d"):format(binary:sub(i, i):byte()):gsub(' ', '0')
5.        formatted_binary = formatted_binary .. dec
6.    end

这将遍历字节码中的每个字符,将它们格式化为转义字节(每个都是包含类似“\097”的代码的字符串,在插值时会转义为“a”)。

这个示例的第 4 行有点密集,所以我将其分解。第一的,

binary:sub(i, i)

从字符串中拉出第 i 个字符。然后

binary:sub(i, i):byte()

返回第 i 个字符的 ascii 整数表示。然后我们格式化它

("\\%3d"):format(binary:sub(i, i):byte())

这给了我们一个像“\ 97”这样的字符串,例如,如果字符是“a”。但这不会正确转义,因为我们需要“\097”,所以我们用“0”替换“”。gsub 返回结果字符串和执行的替换次数,所以我们只取第一个返回值并将其放入“dec”中。我不确定为什么默认情况下“%3d”格式不会用“0”替换空格......哦,好吧。

然后为了执行格式化的二进制字符串,我们需要对其进行转义并将结果传递给“load”。Lua 中的怪异 [[]] 引号不会像 "" 那样进行转义......事实上,我不确定他们是否会进行任何转义。因此,为了创建一个可执行的 Lua 字符串,该字符串将返回一个函数,该函数将执行“some_function”中的任何操作,我们这样做:

executable_string = 'load("' .. formatted_binary .. '")'

好的 - 把所有这些放在一起,我认为我们可以让你的测试用例像这样工作:

  1 function love.load()
  2     draw_before_serialize = function()
  3         love.graphics.print('hello, world', 10, 10)
  4     end
  5 
  6     binary = string.dump(draw_before_serialize)
  7     formatted_binary = ""
  8     for i = 1, string.len(binary) do
  9         dec, _ = ("\\%3d"):format(binary:sub(i, i):byte()):gsub(' ', '0')
 10         formatted_binary = formatted_binary .. dec
 11     end
 12     
 13     out = io.open("serialized.lua", "wb")
 14     out:write('draw = load("' .. formatted_binary .. '")')
 15     out:close()
 16     
 17     require "serialized"
 18 end 
 19 function love.draw()
 20     draw()
 21 end

当我用 Love 运行它时,我得到一个 OpenGL 屏幕,角落里印有“hello world”。生成的文件“serialized.lua”包含以下内容:

draw = load("\027\076\074\001\000\009\064\109\097\105\110\046\108\117\097\084\000\000\004\000\004\000\008\009\002\002\052\000\000\000\055\000\001\000\055\000\002\000\037\001\003\000\039\002\010\000\039\003\010\000\062\000\004\001\071\000\001\000\017\104\101\108\108\111\044\032\119\111\114\108\100\010\112\114\105\110\116\013\103\114\097\112\104\105\099\115\009\108\111\118\101\001\001\001\001\001\001\001\002\000\000")
于 2012-06-08T22:24:31.487 回答