一种在elf中集成脚本文件的方案

进行游戏服务器开发时,我们将C++的部分称之为引擎层,而lua称之为脚本层。但是往往有些核心逻辑是各个游戏公用的, 或者说有些引擎层的代码用C++写起来十分麻烦,我们还是会使用lua来编写。这就带来了一些问题,我们的游戏目录结构如下:

├─bin               // 可执行文件
└─scripts           // 脚本目录,lua文件
    ├─framework     // 核心lua文件,各个项目公用的
    └─server        // 游戏逻辑lua文件

其中scripts/framework是各个项目公用的,并且和bin目录中的可执行文件同时发布和更新。所以有一个想法,就是将framework中 的lua文件集成到可执行文件中,减少维护的成本。

文件存储

下面是elf文件的示意图

elf

elf文件有多个section,除了一些预定义的section如.rodata.text.init等,我们也可以定义一些自己的section。所以我们可以将所需要的lua文件 放进这个section中,在执行的时候动态读出来,实现目的。我们可以使用objcopy命令来实现创建自定义section的功能。

objcopy infile.out --add-section .lua-data=section_file outfile.out

然而framework里面有多个文件,而且包含嵌套的文件夹,我们需要一个将文件夹变成单个文件的功能,类似于tar。虽然创建 section时使用tar命令是简单的,但是在读取的时候需要一些第三方的库来支持,这是比较麻烦的。而由于我们的目录中只包含lua文件,所以可以简化设计。 首先空文件夹对于我们是无意义的,只需要lua文件就可以。所以最终我们得到如下的表:

┌────────────────────┐
│ libs/json.lua      │
├────────────────────┤
│ core/entity.lua    │
├────────────────────┤
│ app/game.lua       │
├────────────────────┤
│ libs/bson.lua      │
└────────────────────┘

我们可以按照如下的格式转换成单个文件

┌────────┬───────────┐
│name_len│content_len│
├────────┴───────────┤
│ core.entity        │
├────────────────────┤
│name_len│content_len│
├────────────────────┤
│ libs.bson          │
├────────────────────┤
│ .................  │
└────────────────────┘

其中name_len为文件名的长度,这里直接转换成了lua中require的格式,使用点符号。content_len是文件内容的长度,即文件的具体内容长度。最后我们可以使用zip指令 将这部分内容压缩存储在elf文件中。完整的代码如下:

#!/usr/bin/env python
#coding: utf-8

import os, struct, StringIO, zlib, subprocess, sys, tempfile, argparse

argParser = argparse.ArgumentParser()

argParser.add_argument("luafolder", type=str)
argParser.add_argument("exe", type=str)
argParser.add_argument("out", type=str)

args = argParser.parse_args()

files = []

path = args.luafolder

for (dirpath, dirname, filenames) in os.walk(path):
    dirp = dirpath[len(path):]
    if dirp:
        if dirp[-1] != "/":
            dirp += "/"

        while dirp[0] == "/":
            dirp = dirp[1:]

    files.extend([dirp + x for x in filenames])

output = StringIO.StringIO()

for fpath in files:
    realp = path + "/" + fpath
    filesize = os.path.getsize(realp)

    if fpath.endswith(".lua"):
        fpath = fpath[:-4]
    elif fpath.endswith(".luac"):
        fpath = fpath[:-5]
    else:
        continue

    package_pattern = fpath.replace("/", ".")
    package_pattern = "pg." + package_pattern
    with open(realp, "rb") as rf:
        content = rf.read()
        output.write(struct.pack("=hL", len(package_pattern), len(content)))
        output.write(package_pattern)
        output.write(content)

f = tempfile.NamedTemporaryFile()

outdata = output.getvalue()
f.write(struct.pack("=L", len(outdata)))
f.write(zlib.compress(output.getvalue()))
f.flush()

subprocess.call("objcopy %s --remove-section .lua-data"%(args.exe, ), shell=True)
subprocess.call("objcopy %s --add-section .lua-data=%s %s"%(args.exe, f.name, args.out), shell=True)

文件内容的读取

我们需要使用elf.h文件来读取文件内容。根据上述的格式示意图,elf文件开头的是Header,其格式为ElfXX_Ehdr, 我们可以直接读取文件内容到内存。然后读取e_shoff字段获得section header的位置,定位到位置并依次读取内容到ElfXX_Shdr 结构体中,然后通过各个entry的sh_name得到最终section,然后读取文件达到目的。完整代码如下:

static std::map<std::string, std::string> readElfLuaData(const std::string &filepath) {
    std::map<std::string, std::string> files;
#if __x86_64__
    typedef Elf64_Ehdr ELF_EHDR;
    typedef Elf64_Shdr ELF_SHDR;
#else
    typedef Elf32_Ehdr ELF_EHDR;
    typedef Elf32_Shdr ELF_SHDR;
#endif

    std::ifstream ifs(filepath);

    ELF_EHDR hdr;
    ifs.read(reinterpret_cast<char *>(&hdr), sizeof(hdr));

    std::vector<ELF_SHDR> sh_tables(hdr.e_shnum);
    ifs.seekg(static_cast<long>(hdr.e_shoff));

    for (size_t i = 0; i < hdr.e_shnum; ++i) {
        ifs.read(reinterpret_cast<char *>(&sh_tables[i]), sizeof(sh_tables[i]));
    }

    // read shstr

    std::vector<char> shstr(sh_tables[hdr.e_shstrndx].sh_size);
    ifs.seekg(static_cast<long>(sh_tables[hdr.e_shstrndx].sh_offset));
    ifs.read(shstr.data(), static_cast<long>(shstr.size()));

    ELF_SHDR *lua_sh = nullptr;

    for (size_t i = 0; i < hdr.e_shnum; ++i) {
        char *name = shstr.data() + sh_tables[i].sh_name;
        if (strcmp(name, ".lua-data") == 0) {
            lua_sh = &sh_tables[i];
            break;
        }
    }

    if (lua_sh) {
        std::vector<char> buf(lua_sh->sh_size);
        ifs.seekg(static_cast<long>(lua_sh->sh_offset));
        ifs.read(buf.data(), static_cast<std::streamsize>(buf.size()));

        size_t idx = 0;
#define READ_TO(TARGET, SIZE)                                                                      \
    memcpy(TARGET, buf.data() + idx, SIZE);                                                        \
    idx += SIZE;

        uint32_t raw_len = 0;
        READ_TO(&raw_len, sizeof(raw_len));

        std::vector<char> tmp(raw_len);
        uLongf dest_len = tmp.size();
        uncompress(reinterpret_cast<Bytef *>(tmp.data()), &dest_len,
                   reinterpret_cast<Bytef *>(buf.data() + idx), buf.size() - idx);

        buf.swap(tmp);
        idx = 0;

        while (idx < dest_len) {
            uint16_t name_len;
            uint32_t content_len;
            READ_TO(&name_len, sizeof(name_len));
            READ_TO(&content_len, sizeof(content_len));
            std::string filename(name_len, 0), content(content_len, 0);
            READ_TO(&*filename.begin(), filename.size());
            READ_TO(&*content.begin(), content.size());
            files.emplace(std::piecewise_construct, std::forward_as_tuple(std::move(filename)),
                          std::forward_as_tuple(std::move(content)));
        }
#undef READ_TO
    }
    return files;
}

接下来便可以通过添加到package.preload实现在lua中调用这些文件的目的。

--EOF--