虚拟文件系统的自举

栏目: Lua · 发布时间: 5年前

内容简介:我们给游戏引擎设计了一个虚拟文件系统,可以挂接不同的文件系统实现,比如本地文件系统模块,内存文件系统模块,网络文件系统模块。比如前几天谈到的资源仓库,就是一个文件系统模块。这个虚拟文件系统是用 lua 编写的,这就有了一个小问题:lua 代码本身也是放在虚拟文件系统中的,那么就需要解决自举。这些代码很有可能需要从网络更新(网络文件系统模块),而网络模块也是 lua 编写的,代码同样放在这套文件系统内。这篇 blog 我想谈谈自举是怎样完成的。

我们给游戏引擎设计了一个虚拟文件系统,可以挂接不同的文件系统实现,比如本地文件系统模块,内存文件系统模块,网络文件系统模块。比如前几天谈到的资源仓库,就是一个文件系统模块。

这个虚拟文件系统是用 lua 编写的,这就有了一个小问题:lua 代码本身也是放在虚拟文件系统中的,那么就需要解决自举。这些代码很有可能需要从网络更新(网络文件系统模块),而网络模块也是 lua 编写的,代码同样放在这套文件系统内。

这篇 blog 我想谈谈自举是怎样完成的。

首先我不想做的太复杂。我们不需要特别弹性的不同文件系统模块挂接到虚拟文件系统的不同目录上的功能。所以我写死了一个叫 .firmware 的目录,专门存放用来自举所需的基础代码(的备用版本)。这块代码在启动后可以在网络模块加载完毕后,用新的版本覆盖。

其次,除了非常必要的 C 代码(例如调用 os 的文件访问 api)外,我希望全部用 lua 实现。

但是,所有 lua 代码,包括文件系统的实现都是放在文件系统内的。我们需要初始化 lua 虚拟机,加载必要的代码,然后才能建立起最低可运行的环境。也就是说,建立这个环境时,lua 虚拟机还并不存在。这样就需要解决先有鸡还是先有蛋的问题。

好在我们先 封装了 lua 虚拟机模块 ,简化了使用。我基于它,创建出一个最小可用的虚拟机环境,只用来读取虚拟文件系统支持模块(先不加载网络模块),然后封装成 C API ,藏起 lua vm 这个细节,专供自举阶段使用。当自举完成,就可以销毁这个虚拟机,在正式的环境重新加载相关 Lua 模块了。

大概是这样的:

struct vfs * vfs_init(const char *firmware, const char *repo);
const char * vfs_load(struct vfs *V, const char *path);
void vfs_exit(struct vfs *V);

初始化的时候,传入一个 firmware 的路径,把自举所需的最小 lua 代码放进去。再传入 repo 仓库路径,供 vfs lua 代码可以工作后,作为新版本替代。

也就是说,我先把 bootstrap 的 lua 代码以原生文件的形式,放在 firmware 里,(针对 ios 版,就是打包在 app 中)一开始加载出来使用。用这块代码创建出可以访问 repo 的最小环境(但不包括网络功能)。repo 是我们的资源仓库,里面有上次从网络上同步过来的最新代码。然后,我们从 repo 中再次加载新版本的 vfs 支持代码,之后用最新版本的 vfs 支持代码去访问 repo 仓库中的其它部分。

一旦新版本出了问题,也可以直接把 repo 删干净,回退到最老的 firmware 版本上。

vfs 的 C 实现尽量少嵌入写死的 lua 代码,大约是这样的:

struct vfs {
    struct luavm *L;
    int handle;
};

static int
linitvfs(lua_State *L) {
    luaL_checktype(L,1, LUA_TLIGHTUSERDATA);
    struct vfs ** V = (struct vfs **)lua_touserdata(L, 1);
    *V = lua_newuserdata(L, sizeof(struct vfs));
    return 1;
}

extern int luaopen_winfile(lua_State *L);

static int
lfs(lua_State *L) {
    // todo: use lfs
    return luaopen_winfile(L);
}

static int
cfuncs(lua_State *L) {
    luaL_checkversion(L);
    luaL_Reg l[] = {
        { "initvfs", linitvfs },        
        { "lfs", lfs },
        { NULL, NULL },
    };
    luaL_newlib(L, l);
    return 1;
}

static const char * init_source = "local _, firmware = ... ; loadfile(firmware .. '/bootstrap.lua')(...)";

struct vfs *
vfs_init(const char *firmware, const char *dir) {
    struct luavm *L = luavm_new();
    if (L == NULL)
        return NULL;
    struct vfs *V = NULL;
    const char * err = luavm_init(L, init_source, "ssfp", firmware, dir, cfuncs, &V);
    if (err) {
        fprintf(stderr, "Init error: %s\n", err);
        luavm_close(L);
        return NULL;
    }
    if (V == NULL) {
        luavm_close(L);
        return NULL;
    }

    V->L = L;
    err = luavm_register(L, "return _LOAD", "=vfs.load", &V->handle);
    if (err) {
        // register failed
        fprintf(stderr, "Register error: %s\n", err);
        luavm_close(L);
        return NULL;
    }
    return V;
}

void
vfs_exit(struct vfs *V) {
    if (V) {
        luavm_close(V->L);
    }
}

const char *
vfs_load(struct vfs *V, const char *path) {
    const char * ret = NULL;
    const char * err = luavm_call(V->L, V->handle, "sS", path, &ret);
    if (err) {
        fprintf(stderr, "Load error: %s\n", err);
        return NULL;
    }
    return ret;
}

这里在初始化的时候仅仅是用原生的 loadfile 读入了 bootstrap.lua 这个文件而已。所以可以在不动任何 C 代码的基础上做到业务逻辑的更新。

最后来看看 bootstrap.lua 的实现:

local errlog, firmware, dir, cfuncs, V = ...

cfuncs = cfuncs()

package.preload.lfs = cfuncs.lfs    -- init lfs

local vfs = assert(loadfile(firmware .. "/vfs.lua"))()
local repo = vfs.new(firmware, dir)
local f = repo:open(".firmware/vfs.lua")    -- try load vfs.lua in vfs
if f then
    local vfs_source = f:read "a"
    f:close()
    vfs = assert(load(vfs_source, "@.firmware/vfs.lua"))()
    repo = vfs.new(firmware, dir)
end

local function readfile(f)
    if f then
        local content = f:read "a"
        f:close()
        return content
    end
end

local bootstrap = readfile(repo:open(".firmware/bootstrap.lua"))

if bootstrap then
    local newboot = load(bootstrap, "@.firmware/bootstrap.lua")
    local selfchunk = string.dump(debug.getinfo(1, "f").func, true)

    if string.dump(newboot, true) ~= selfchunk then
        -- reload bootstrap
        newboot(...)
        return
    end
end

function _LOAD(path)
    local f = repo:open(path)
    if f then
        local content = f:read "a"
        f:close()
        return content
    end
end

_VFS = cfuncs.initvfs(V)    -- init V , store in _G

它会去加载 vfs.lua 建立一个最小环境,然后用新加载出来的 vfs 模块,重加载 bootstrap.lua 自身,看是否有更新,最终保证采用的是仓库中最新的代码来加载文件。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

两周自制脚本语言

两周自制脚本语言

[日]千叶 滋 / 陈筱烟 / 人民邮电出版社 / 2014-6 / 59.00元

《两周自制脚本语言》是一本优秀的编译原理入门读物。全书穿插了大量轻松风趣的对话,读者可以随书中的人物一起从最简单的语言解释器开始,逐步添加新功能,最终完成一个支持函数、数组、对象等高级功能的语言编译器。本书与众不同的实现方式不仅大幅简化了语言处理器的复杂度,还有助于拓展读者的视野。 《两周自制脚本语言》适合对编译原理及语言处理器设计有兴趣的读者以及正在学习相关课程的大中专院校学生。同时,已经......一起来看看 《两周自制脚本语言》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具