lua5.3 注册表与全局变量

注册表

注册表( registry)是张只能被代码访问的全局表,这个注册表实际上就是一个普通的Lua表。它主要的作用,

  • 可以存放 lua 的全局变量
  • 存储 c 编写的扩充函数

注册表预先定义了一些key/value。

  • LUA_RIDX_MAINTHREAD:指向 主线程
  • LUA_RIDX_GLOBALS:存储全局变量
  • LUA_LOADED_TABLE:存储模块(包括系统默认加载的模块,以及用户 require 的模块)

在 lua 中,我们可以通过 _G 或者 _ENV 来访问 LUA_RIDX_GLOBALS 表,通过 _G.package.loaded 来访问 LUA_LOADED_TABLE 表。

在 c 扩展中,可以通过 lua_getglobal api来访问 LUA_RIDX_GLOBALS 表,通过 luaL_requiref 来访问 LUA_LOADED_TABLE 表。

伪索引

注册表由伪索引 LUA_REGISTRYINDEX(-1001000)来定位,伪索引就像是栈中的一个索引,但它所关联的值不在栈中。在 lua 源码中,我们可以看到如何访问注册表的:

static TValue *index2addr (lua_State *L, int idx) {
  CallInfo *ci = L->ci;
  if (idx > 0) {  // 索引是正数,在栈上
    TValue *o = ci->func + idx;
    ...
    return o;
  }
  else if (!ispseudo(idx)) {  /* negative index  索引是负数,且在栈上*/
    ...
    return L->top + idx;
  }
  else if (idx == LUA_REGISTRYINDEX) // 访问注册表
    return &G(L)->l_registry;
  else {  /* upvalues 上值索引 */
    idx = LUA_REGISTRYINDEX - idx;
    ...
    if (ttislcf(ci->func))  /* light C function? */
      return NONVALIDVALUE;  /* it has no upvalues */
    else {
      CClosure *func = clCvalue(ci->func);
      return (idx <= func->nupvalues) ? &func->upvalue[idx-1] : NONVALIDVALUE;
    }
  }
}

为了方便统一管理,都由 index2addr 函数处理,索引来获取值有4种情况:

  • 索引为正数,值一定是在栈上的,通过 ci->func + idx 来获取值。
  • 索引为负数,且大于 LUA_REGISTRYINDEX 的情况,表明值是通过栈顶 L->top + idx 来获取。
  • 索引为负数,且等于 LUA_REGISTRYINDEX,访问注册表。
  • 索引为负数,且小于 LUA_REGISTRYINDEX,表明访问的是闭包函数的上值。

 

全局变量

在编写 lua 代码中,变量如果没有定义成 local,那么我们就当作全局变量,在其他模块中也能访问到。那这些全局变量都是放在一个叫 _ENV 指向的表中,而 _ENV 默认是指向 LUA_RIDX_GLOBALS 表,与此同时,我们还有一个 _G 的全局变量,也是指向  LUA_RIDX_GLOBALS 表。那么,_ENV 和 _G 有什么不一样的地方呢。

根据 lua 官方的介绍,每当加载一个 lua 代码文件,或者加载字符串时(我们对其简称 chunk),都会为其生成一个闭包函数,并初始化 chunk 的第一个upvalue为 _ENV。所以,我们平时访问的全局变量,其实是访问 _ENV 指向的那个表。当然,我们也可以自己指定 _ENV 的来源。如果 _ENV 指向新的表,那么就和 _G 不是同一个表了,这就是它们的区别。

t = 12
print(t)

比如,上面两行 lua 代码,通过 luac -l -l aaa.lua 查看文件对应的指令生成:

main <aaa.lua:0,0> (5 instructions at 0000000000b284c0)
0+ params, 2 slots, 1 upvalue, 0 locals, 3 constants, 0 functions
        1       [1]     SETTABUP        0 -1 -2 ; _ENV "t" 12
        2       [2]     GETTABUP        0 0 -3  ; _ENV "print"
        3       [2]     GETTABUP        1 0 -1  ; _ENV "t"
        4       [2]     CALL            0 2 1
        5       [2]     RETURN          0 1
constants (3) for 0000000000b284c0:
        1       "t"
        2       12
        3       "print"
locals (0) for 0000000000b284c0:
upvalues (1) for 0000000000b284c0:
        0       _ENV    1       0

可以看到,第 [1] 行生成一个 SETTABUP 指令,意思是将 key = "t",value = 12 设置到 _ENV 这个表中。由此可知,对全局变量,或者全局函数的访问,都是访问 _ENV 指向的那个表。

等同于下面的代码:

_ENV.t = 12
_ENV.print(_ENV.t)

如果要细看源码的话,在解析阶段时,就会默认给一段 chunk 加上一个上值 _ENV。

// lapi.c
LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
                      const char *chunkname, const char *mode) {
  ...
  // 解析 chunk 
  status = luaD_protectedparser(L, &z, chunkname, mode);
  if (status == LUA_OK) {  /* no errors? */
    LClosure *f = clLvalue(L->top - 1);  /* get newly created function */
    if (f->nupvalues >= 1) {  /* does it have an upvalue? */
      /* get global table from registry */
      // 获取注册表
      Table *reg = hvalue(&G(L)->l_registry);
        
      // 获取全局表 LUA_RIDX_GLOBALS
      const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS);
        
      /* set global table as 1st upvalue of 'f' (may be LUA_ENV) */
      // 解析完 lua 代码后,这里默认设置闭包第一个上值为LUA_RIDX_GLOBALS 指向的表
      setobj(L, f->upvals[0]->v, gt);
      ...
}

/*
** lparser.c
** compiles the main function, which is a regular vararg function with an
** upvalue named LUA_ENV
*/
static void mainfunc (LexState *ls, FuncState *fs) {
  ...
  init_exp(&v, VLOCAL, 0);  /* create and... */
  newupvalue(fs, ls->envn, &v);  /* ...set environment upvalue 给解析的代码块创建第一个默认上值 _ENV */
  ...
}

// llex.c 初始化词法解析对象 llex
void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source,
                    int firstchar) {
  ...
  ls->envn = luaS_newliteral(L, LUA_ENV);  /* get env name 生成一个 "_ENV" 字符串 */
  ...
}

// llex.h 
#define LUA_ENV		"_ENV"

在 lparser.c 中,解析变量的时候,发现在局部变量中,查找不到对应的定义,且当前可见的作用域也找不到这么一个上值变量时,最终会去查找 _ENV 这个表,如果 _ENV 里面没有存在这个变量,也不会再去其他地方找了,最终会生成一条 GETTABUP 指令,对应 _ENV[变量名]。

static void singlevar (LexState *ls, expdesc *var) {
  TString *varname = str_checkname(ls);
  FuncState *fs = ls->fs;
  singlevaraux(fs, varname, var, 1);
  if (var->k == VVOID) {  /* global name? 没有找到变量在哪定义的,全局变量名? */
    expdesc key;
    singlevaraux(fs, ls->envn, var, 1);  /* get environment variable 这个 ls->envn 指向 _ENV 上值 */
    lua_assert(var->k != VVOID);  /* this one must exist */
    codestring(ls, &key, varname);  /* key is variable name */
    luaK_indexed(fs, var, &key);  /* env[varname] 最后,变成访问 _ENV[变量名] */
  }
}

// llex.c
void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source,
                    int firstchar) {
  ...
  ls->envn = luaS_newliteral(L, LUA_ENV);  /* get env name */
  ...
}

// llex.h
#if !defined(LUA_ENV)
#define LUA_ENV		"_ENV"
#endif

在解析完 lua 文件后,把里面解析的指令都封装到一个新的 lua 闭包函数里,并向栈顶 push 这个闭包,然后设置第一个上值为 _ENV

LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
                      const char *chunkname, const char *mode) {
  ...
  status = luaD_protectedparser(L, &z, chunkname, mode);
  if (status == LUA_OK) {  /* no errors? */
      // 获取全局注册表
      Table *reg = hvalue(&G(L)->l_registry);
      const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS);
      /* set global table as 1st upvalue of 'f' (may be LUA_ENV) ;设置 lua 闭包函数上值 _ENV */
      setobj(L, f->upvals[0]->v, gt);
      ...
}

 

如果我们修改了 _ENV 的值,那么此时,新的全局变量就会放到 _ENV 指向的表里头,旧的全局变量就访问不到了,得提前存起来了。

t = 12
print(t)

local _G = _ENV._G
local print = _ENV.print
_ENV = {} -- _ENV 指向新的 {}
print(t) -- 打印 t 的值,为 nil
print(_ENV, _G)
--[[
运行结果:
12
nil
table: 00000000006a9e90 table: 00000000006a1820
]]

再看一个改变 _ENV 例子:

local print = print

local function f1()
	_ENV = {}
	a = 12
	print(a)
end

f1()

print(_ENV.a)

--[[
运行结果:
12
12
]]

可以看到,_ENV 即使在函数里改变值,也会影响函数外的 _ENV,因为在这个代码文件中 _ENV 不管是函数内,还是函数外,都是访问同一个上值变量。

_G 的实现就不一样了。它是在 lua 使用标准库时创建的,初始化时就会被指向 LUA_RIDX_GLOBALS 表。代码实现在 lbaselib.c 文件中。 

// lbaselib.c
LUAMOD_API int luaopen_base (lua_State *L) {
  /* open lib into global table */
  lua_pushglobaltable(L); // 加载全局表 LUA_RIDX_GLOBALS 到栈上
  luaL_setfuncs(L, base_funcs, 0);
  /* set global _G */
  lua_pushvalue(L, -1);
  lua_setfield(L, -2, "_G"); // 将 "_G" 变量指向 LUA_RIDX_GLOBALS 表
  /* set global _VERSION */
  lua_pushliteral(L, LUA_VERSION);
  lua_setfield(L, -2, "_VERSION");
  return 1;
}

如果当我们也改变 _G 的值时,_ENV 会受影响吗,答案是不会,因为它们两个是不同的变量名,在 lua 中,不同的变量名可以指向同一个值,也可以指向不同的值,改变其中一个变量名的指向,不会影响另一个变量名,虽然这两个变量名比较特殊,是系统提供好的变量名,但和其他用户定义的变量名用法上没太多区别。

t = 12
print(t)

_G = {}
print(t)
print(_ENV.t, _G.t)

--[[
运行结果:
12
12
12      nil
]]

全局环境应用

像某些分区游戏,我们可以用 _ENV 做环境隔离,实现一个区对应一份环境,一个进程加载多个区服数据。

实现思路如下:

文件目录如下:

先编写一个类似 require 的模块,叫 require_ex.lua :

-- require_ex.lua
local mod = {}
local zenv = {}

local __envmt = {__index = _G}

-- 初始化有几个区
function mod.init(zone)
    zenv = {}
    for i,v in ipairs(zone) do
        local o = setmetatable({C_ZONE_ID = v}, __envmt)
        zenv[v] = {env = o, loaded = {}}
    end
end

-- 每个区号对应一份 _ENV 环境
function mod.require_aux(zoneid, modname)
    local data = zenv[zoneid]
    local m = data.loaded[modname]
    if m ~= nil then
        return m
    else
        local path = modname .. ".lua" -- 这个只是简单路径拼接,可以根据实际需求来改动
        local f = loadfile(path, "t", data.env)
        m = f()
        if m == nil then
            m = true
        end
        data.loaded[modname] = m
        return m
    end
end

local function require_zx(modname)
    local f = debug.getinfo(2, "f").func
    local _, _env = debug.getupvalue(f, 1)
    return mod.require_aux(_env.C_ZONE_ID, modname)
end

_G.require_zx = require_zx

return mod

 再编写两个模块文件,一个 player.lua 文件:

local attr = require_zx("attr") -- 加载其他模块文件,这里不用加区号,因为 player.lua 和 attr.lua 在同一个 _ENV 下
local player = {}

function player.do_msg(data)
    -- 打印区号
    print("player do_msg:", C_ZONE_ID, attr.skill, attr.power)
    for k,v in pairs(data) do
        print(k,v)
    end
end

return player

一个 attr.lua 文件:

-- 打印区号
print("attr module zone id:", C_ZONE_ID)

return {
    skill = C_ZONE_ID + 1,
    power = C_ZONE_ID + 100,
}

最终,我们编写一个 main.lua 文件,模拟测试程序处理 lua 消息:

local modmng = require "require_ex"

-- 模拟处理客户端发来的消息
function handle_msg(zoneid, modname, data)
    local mod = modmng.require_aux(zoneid, modname)
    mod.do_msg(data)
end

------------------------------- 模拟测试消息接收入口 -------------------------------
local function test()
    -- 当前进程启动时,读取配置,要加载的区服有哪些
    modmng.init({1, 2, 5})

    print("----------模拟 2 区消息处理----------")
    handle_msg(2, "player", {a = 12, b = false})

    print("----------模拟 5 区消息处理----------")
    handle_msg(5, "player", {a = 6, b = true})
end

test()

--[[
运行结果:
----------模拟 2 区消息处理----------
attr module zone id:    2
player do_msg:  2       3       102
a       12
b       false
----------模拟 5 区消息处理----------
attr module zone id:    5
player do_msg:  5       6       105
a       6
b       true
]]

 我们可以看到不同区的,player 模块和 attr 模块打印的区号 C_ZONE_ID 不一样,说明做到了不同的区之间的文件相互隔离。

在实现一些工具类模块时,我们则可以使用 require API,因为 require 文件,默认是加载到全局模块中的。但某些,API 接口设计,有时候想要知道是被哪个区调用的,有两种方式实现,一种是接口明确指定区号id,另一种就是通过下面两个API 接口获取:

local function xxx(modname)
    local f = debug.getinfo(2, "f").func
    local _, _env = debug.getupvalue(f, 1)
    local zoneid = _env.C_ZONE_ID
    -- 其他逻辑...
end

debug.getinfo(2, "f")第一个参数可以为数字,或者函数,数字时,表示获取某层函数信息,0表示获取当前层的函数信息,即 getinfo 函数,1表示获取 xxx 函数信息,2 表示获取调用 xxx 之上的一层函数信息。

debug.getupvalue表示获取某个函数的上值,1表示获取函数 f 第一个上值 _ENV。

在 _ENV 内部模块之间引用其他模块,是通过 require_zx函数实现的,可以做到模块之间不需要指定区号,对使用者来说,是透明的,我们正常编写代码即可,但在客户端上传的消息中,我们需要知道消息是针对哪个区的模块去处理的。

 

posted @ 2024-05-04 17:14  墨色山水  阅读(363)  评论(0)    收藏  举报