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函数实现的,可以做到模块之间不需要指定区号,对使用者来说,是透明的,我们正常编写代码即可,但在客户端上传的消息中,我们需要知道消息是针对哪个区的模块去处理的。

浙公网安备 33010602011771号