lua环境与模块

Lua实现了一个安全的运行环境、一套自动内存管理机制、优秀的字符串处理能力和动态大小数据的处理功能。

我们都知道,只要应用程序加入lua解析器的功能,就能解析lua脚本。那lua脚本是怎样执行的?
我们通常用dofile去打开编译一个lua脚本。当应用程序调用dofile后,在执行完脚本后,才能后到主程序中,这lua脚本相当于一个函数。要等函数执行完才能回到主程序中。
lua脚本中,一般有变量,函数,这些东西都保存在一个常规的table中,这个table称为“环境”。

loadfile、dofile、require

loadfile——只加载编译,不运行
dofile——执行
require——只执行一次

require函数只能加载一次,因为它的特性是:

1、require函数会搜索目录加载文件
2、require会判断是否文件已经加载避免重复加载同一文件

但当有一些特殊的需求需要反复加载某个lua文件,那如何实现反复加载一个lua文件?
二次加载前加这一句:

1
package.loaded[luafile] = nil

环境相关的变量

这里首先分析几个与环境相关的特殊变量一Global表 、 env表 、 registry表以及UpValue
关于前3个表,需要注意以下几点:

  • Global表存放在lua State结构体中也称为G表 。 每个lua State结构体都有一个对应的G表。 不用多说,这个表就是存放全局变量的。
  • env表存放在Closure结构体中,也就是每个函数有自己独立的一个环境 。
  • registry表是全局唯一的,它存放在global_State结构体中,这个结构体在整个运行环境中只有一个。

查找一个全局变量的操作,其实更精确地说,是在当前函数的env表中查找 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case OP_GETGLOBAL: {
TValue g;
TValue *rb = KBx(i);
sethvalue(L, &g, cl->env);
lua_assert(ttisstring(rb));
Protect(luaV_gettable(L, &g, rb, ra));
continue;
}
case OP_SETGLOBAL: {
TValue g;
sethvalue(L, &g, cl->env);
lua_assert(ttisstring(KBx(i)));
Protect(luaV_settable(L, &g, KBx(i), ra));
continue;
}

可以看到,这两个操作都是到函数对应的Closure指针中的env表去查询数据 。 这里仍然需要提醒一下前面提到的一点,即使对一个没有任何函数的代码而言,分析完毕之后都对应一个Closure。 因此,这里提到的“当前函数环境”,指的不一定是某一个具体的函数,也可能是一个Lua文件 。

Lua提供了几个API来读取当前函数的环境,分别是getfenvsetfenv
因此,如果执行以下代码:

1
2
setfenv(1,{})
print(a)

实际上找不到Lua标准库提供的print函数,并且会提示报错attempt to call global ’a’(a nil value) 。 原因就是首先使用 setfenv函数将当前函数的env表置为一个空表,此时在当前函数的env表中查找不到这个名字的函数。

下面来看看函数的env表是如何创建的 。
在创建一个Closure对象时,都会调用getcurrenv函数来获取当前的环境表:

1
2
3
4
5
6
7
8
static Table *getcurrenv (lua_State *L) {
if (L->ci == L->base_ci) /* no enclosing function? *///如果该函数不是内嵌函数,那么直接返回G表
return hvalue(gt(L)); /* use global table as environment */
else {//再则,如果是内嵌函数,就返回其母函数的`env`表
Closure *func = curr_func(L);
return func->c.env;
}
}

在创建一个新的Closure时,会调用这个函数返回的结果,对新的Closure的环境进行赋值。这里可以看出, env表会逐层继承。

接着来看看registry表的作用,该表存放在global_State结构体中,因此里面的内容可供多个lua State访问 。 另外,这个表只能由C代码访问,Lua代码不能访问 。 除此之外,它和普通的表没有什么区别 。

但是需要注意的是,使用普通的对表进行赋值的APIregistry表进行赋值时,应该使用字符串类型的键。LuaAPI中对外提供了接口lua_reflua_unref于和lua_getref,用于提供在registry表中存取唯一的数字键。 通过这组API,使用者不需要关心给某个需要存放到registry表的数据如何分配一个全局唯一的键,由Lua解释器自己来保证这一点:

1
2
3
4
5
6
#define lua_ref(L,lock) ((lock) ? luaL_ref(L, LUA_REGISTRYINDEX) : \
(lua_pushstring(L, "unlocked references are obsolete"), lua_error(L), 0))

#define lua_unref(L,ref) luaL_unref(L, LUA_REGISTRYINDEX, (ref))

#define lua_getref(L,ref) lua_rawgeti(L, LUA_REGISTRYINDEX, (ref))

接着来看看这里面lual refluaL_unref函数的实现。 需要说明的是,在调用luaL_ref函数之前,需要存放的数据已经位于栈顶:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
LUALIB_API int luaL_ref (lua_State *L, int t) {
int ref;
t = abs_index(L, t);
if (lua_isnil(L, -1)) {
lua_pop(L, 1); /* remove from stack */
return LUA_REFNIL; /* `nil' has a unique fixed reference */
}
lua_rawgeti(L, t, FREELIST_REF); /* get first free element */
ref = (int)lua_tointeger(L, -1); /* ref = t[FREELIST_REF] */
lua_pop(L, 1); /* remove it from stack */
if (ref != 0) { /* any free element? */
lua_rawgeti(L, t, ref); /* remove it from list */
lua_rawseti(L, t, FREELIST_REF); /* (t[FREELIST_REF] = t[ref]) */
}
else { /* no free elements */
ref = (int)lua_objlen(L, t);
ref++; /* create new reference */
}
lua_rawseti(L, t, ref);
return ref;
}

LUALIB_API void luaL_unref (lua_State *L, int t, int ref) {
if (ref >= 0) {
t = abs_index(L, t);
lua_rawgeti(L, t, FREELIST_REF);
lua_rawseti(L, t, ref); /* t[ref] = t[FREELIST_REF] */
lua_pushinteger(L, ref);
lua_rawseti(L, t, FREELIST_REF); /* t[FREELIST_REF] = ref */
}
}

这里的设计其实很巧妙,仅使用一个数组就模拟了一个链表的实现,其原理如下:

  • FREELIST_REF用于保存当前registry表中可用键的索引,每次需要存储之前,都会先到这里拿到当前存放的值。
  • 如果拿出来的值是0 ,说明当前的hashlist中还没有数据,直接返回当前registry表的数据量作为新的索引 。
  • 当调用lual unref释放一个索引值的时候,将该索引值返回FREELIST REF链表中 。

下图演示了分配可用索引前后freelist的变化
捕获
最后来看UpValue。 前面谈到,registry表提供的是全局变量的存储, env表提供的是函数内全局变量的存储,而UpValue用于提供函数内静态变量的存储,这些变量存储的地方,倒不是某个特殊的表,其实就是换算成对应的UpValue的索引值来访问函数的UpValue数组而已。
接着我们来看一个关键的函数index2adr,这个函数集中处理了所有索引值转换为栈地址值的操作,不论该索引是栈上元素的索引,还是前面这几种特殊变量的索引:

1
2
3
4
#define LUA_REGISTRYINDEX	(-10000)
#define LUA_ENVIRONINDEX (-10001)
#define LUA_GLOBALSINDEX (-10002)
#define lua_upvalueindex(i) (LUA_GLOBALSINDEX-(i))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static TValue *index2adr (lua_State *L, int idx) {
if (idx > 0) {
TValue *o = L->base + (idx - 1);
api_check(L, idx <= L->ci->top - L->base);
if (o >= L->top) return cast(TValue *, luaO_nilobject);
else return o;
}
else if (idx > LUA_REGISTRYINDEX) {
api_check(L, idx != 0 && -idx <= L->top - L->base);
return L->top + idx;
}
else switch (idx) { /* pseudo-indices */
case LUA_REGISTRYINDEX: return registry(L);
case LUA_ENVIRONINDEX: {
Closure *func = curr_func(L);
sethvalue(L, &L->env, func->c.env);
return &L->env;
}
case LUA_GLOBALSINDEX: return gt(L);
default: {
Closure *func = curr_func(L);
idx = LUA_GLOBALSINDEX - idx;
return (idx <= func->c.nupvalues)
? &func->c.upvalue[idx-1]
: cast(TValue *, luaO_nilobject);
}
}
}

这段代码的逻辑主要是根据传人的idx的几种情况,分别返回不同的值。

  • 如果 idx >O ,那么以 idx值为索引,返回基于 lua State的 base指针的值,也就是相对于战底向上的偏移值。
  • 如果 idx>LUA_REGISTRYINDEX ,则以 idx值为索引,返回基于 l ua_State的top指针的值,也就是相对于钱顶向下的偏移值。
  • 如果是LUA_REGISTRYINDEX ,那么返回 registry表。
  • 如果是LUA ENVIRONINDEX ,那么返回当前函数的env表。
  • 如果是LUA GLOBALSINDEX ,那么返回Global表。
  • 如果以上都不符合,那么将根据情况返回当前函数的叩value数组中的值。

模块

这一节将讲解Lua模块相关的知识点,首先介绍模块的加载、编写等原理,然后介绍热更新原理。

模块的加载

Lua内部,所有模块的注册都在linit.c的函数lual_openlibs中提供。 可以看到,它依次访问lualibs数组中的成员,这些成员定义了每个模块的模块名及相应的模块注册函数,依次调用每个模块的注册函数完成模块的注册 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static const luaL_Reg lualibs[] = {
{"", luaopen_base},
{LUA_LOADLIBNAME, luaopen_package},
{LUA_TABLIBNAME, luaopen_table},
{LUA_IOLIBNAME, luaopen_io},
{LUA_OSLIBNAME, luaopen_os},
{LUA_STRLIBNAME, luaopen_string},
{LUA_MATHLIBNAME, luaopen_math},
{LUA_DBLIBNAME, luaopen_debug},
{NULL, NULL}
};

LUALIB_API void luaL_openlibs (lua_State *L) {
const luaL_Reg *lib = lualibs;
for (; lib->func; lib++) {
lua_pushcfunction(L, lib->func);
lua_pushstring(L, lib->name);
lua_call(L, 1, 0);
}
}

结构体lual_Reg有两个变量,分别是模块名以及模块初始化函数。 可以看到,第一个模块是base模块,其模块名是一个空字符串,因此访问这个模块的函数不需要加模块名前缀,比如我们熟悉的print函数就是属于这个模块的 。 这就是在调用print函数时,不需要在前面加模块名前缀的原因 。 这里就以base模块为例来讲解模块的注册过程。

加载base模块最终会调用 base_open函数,下面我们看看这个函数里面最核心的几行代码:

1
2
3
4
5
6
static void base_open (lua_State *L) {
/* set global _G */
lua_pushvalue(L, LUA_GLOBALSINDEX);
lua_setglobal(L, "_G");
/* open lib into global table */
luaL_register(L, "_G", base_funcs);

最开始的两句首先将 LUA_GLOBA LSINDEX对应的值压人拢中,接着调用 lua_setglobal(L ,二C ”) ; , e n 当在 lua_State 的 l_gt表中查找工C”时,查找到的是索引值为 LUA_GLOBALSINDEX的表 。如果觉得有点绕,可以简单理解为,在C表满足这个等式_G = _G [二G”] 。 也就是这个叫_G的表内部有一个key为二G”的表是指向自己的 。 可以在Lua命令行中执行print(_G )和 print(_G [”_G”])看看输出结果,来验证一下这个结论。

我猜想这么处理的理由是 : 为了让G表和其他表使用同样的机制 。 查找变量时,最终会一直顺着层次往上查到G表中,这是很自然的事情 。 所以,为了也能按照这个机制顺利地查找到自己,于是在G表中有一个同名成员指向自己 。

好了,前两句的作用已经分析完毕,其结果有以下两个 :

  • _G = _G [”_G”]
  • G表的值压入函数枝中方便后面的调用 。

所以,这个G表的注册操作需要在所有模块注册之前进行。

在第63 1行中, base_fu n cs也是一个lual_Reg数组,上面的操作会将base_funcs数组中的函数注册到G表中,但是里面还有些细节需要看看。 这个操作最终会调用函数luaI←openlib:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LUALIB_API void luaI_openlib (lua_State *L, const char *libname,
const luaL_Reg *l, int nup) {
if (libname) {
int size = libsize(l);
/* check whether lib already exists */
luaL_findtable(L, LUA_REGISTRYINDEX, "_LOADED", size);
lua_getfield(L, -1, libname); /* get _LOADED[libname] */
if (!lua_istable(L, -1)) { /* not found? */
lua_pop(L, 1); /* remove previous result */
/* try global variable (and create one if it does not exist) */
if (luaL_findtable(L, LUA_GLOBALSINDEX, libname, size) != NULL)
luaL_error(L, "name conflict for module " LUA_QS, libname);
lua_pushvalue(L, -1);
lua_setfield(L, -3, libname); /* _LOADED[libname] = new table */
}
lua_remove(L, -2); /* remove _LOADED table */
lua_insert(L, -(nup+1)); /* move library table to below upvalues */
}
for (; l->name; l++) {
int i;
for (i=0; i<nup; i++) /* copy upvalues to the top */
lua_pushvalue(L, -nup);
lua_pushcclosure(L, l->func, nup);
lua_setfield(L, -(nup+2), l->name);
}
lua_pop(L, nup); /* remove upvalues */
}

注册这些函数之前,首先会到registry [二LOADED"]表中查找该库,如果不存在,则在G表中查找这个库,若不存在则创建一个表。
因此,不管是Lua内部的库还是外部使用require引用的库,首先会到 registry [”一LOADED ”] 中存放该库的表。 最后,再遍历传进来的函数指针数组,完成库函数的注册。
比如,注册as . print时,首先将print函数绑定在一个函数指针上,再去l_registry[_LOADED]和G表中查询名为OS的库是否存在,不存在则创建一个表,即 :

1
G[”OS"] = {}

紧跟着注册print函数,即: G [”os ”][ ” print ”]=待注册的函数指针。
这样在调用os . print(1)时,首先根据OS到G表中查找对应的表,再在这个表中查找print成员得到函数指针,最后完成函数的调用 。

模块的编写

在定义Lua模块时,第一句代码一般都是module(xxx ) 。 module调用的对应C函数是loadlib.c中的函数ll_module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static int ll_module (lua_State *L) {
const char *modname = luaL_checkstring(L, 1);
int loaded = lua_gettop(L) + 1; /* index of _LOADED table */
lua_getfield(L, LUA_REGISTRYINDEX, "_LOADED");
lua_getfield(L, loaded, modname); /* get _LOADED[modname] */
if (!lua_istable(L, -1)) { /* not found? */
lua_pop(L, 1); /* remove previous result */
/* try global variable (and create one if it does not exist) */
if (luaL_findtable(L, LUA_GLOBALSINDEX, modname, 1) != NULL)
return luaL_error(L, "name conflict for module " LUA_QS, modname);
lua_pushvalue(L, -1);
lua_setfield(L, loaded, modname); /* _LOADED[modname] = new table */
}
/* check whether table already has a _NAME field */
lua_getfield(L, -1, "_NAME");
if (!lua_isnil(L, -1)) /* is table an initialized module? */
lua_pop(L, 1);
else { /* no; initialize it */
lua_pop(L, 1);
modinit(L, modname);
}
lua_pushvalue(L, -1);
setfenv(L);
dooptions(L, loaded - 1);
return 0;
}

代码的前半部分首先根据module(XXX)中的模块名去registry [“_LOADED”]表中查找,如果找不到,则创建一个新表,这个表为_G [” xxx叮= registry [二LOADED ”][ “XXX "] 。 换言之,这个名为xxx 的模块本质上是一个表,这个表存储了这个模块中的所有变革-以及函数,它既可以通过一G [” xxx”]来访问,也可以通过registry [二 LOADED ”][ “XXX ”]来访问 。
紧跟着,在modi nit 函数中,将这个表的成员 K NAME 、 PACKAGE分别赋值。
最后,调用 setfenv将该模块对应的环境置空 。 根据前面的分析, setfenv将该模块对应的环境置空就是将这个模块分析完毕之后返回的Closure对应的env环境表置空 。 这意味着,前面的所有全局变量都看不见了,比如下面的代码中 :

1
2
3
4
myprint=print
myprint(”1”)
module(”test”)
myprint (” 2”)

这里首先将全局函数printl赋值给全局变量myprint ,第二行代码可以正常调用这个函数。但当调用module声明 test模块之后,在此之前的全局变量myprint被清空,第四行代码调用myprint函数时就会报错,错误信息是attempt to call global ‘myprint’(a nil value),因为此时已经查不到这个变量了 。
如果写下的是module(xxx,package . seeall)呢?它将会调用后面的dooptions 函数并且最后调用 package.seeall对应的处理函数:

1
2
3
4
5
6
7
8
9
10
11
static int ll_seeall (lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
if (!lua_getmetatable(L, 1)) {
lua_createtable(L, 0, 1); /* create new metatable */
lua_pushvalue(L, -1);
lua_setmetatable(L, 1);
}
lua_pushvalue(L, LUA_GLOBALSINDEX);
lua_setfield(L, -2, "__index"); /* mt.__index = _G */
return 0;
}

这个函数就两个作用 : 一个是创建该模块对应表的metatable , 另一个是将meta表的 index指向 G表。 也就是说,所有在该模块中找不到的变量都会去 G表中查找 。 可以看到,这里的操作并不会把环境表清空 。 因此,如果把前面的代码改成这样,就可以正确执行:

1
2
3
4
myprint=print
myprint(” test ”)
morlule (”test '’, package. seeall)
myprint(”test”)

根据前面对module函数的分析,得出以下几个结论。

  • 创建模块时会创建一个表,该表挂载在registry [ ”一LOADED ' ’ ]、_G [模块名]下 。 自然而然地,该模块中的变量(函数也是一种变量)就会挂载到这个表里面 。
  • 在 module 函数的参数中写下 package.seeall将会创建该表的 metatable ,同时该表的index将指向 G表。 简单地说,这个模块将可以看到所有全局环境下的变量(这里再提醒一次,函数也是一种变量) 。

明白了 module 背后的作用,再来看看 require 函数,它对应的处理函数是 loadlib.c 中的ll_require 函数,这个函数做了如下几件事情 。

  • 首先在 registry[ “_LOADED”]表中查找该库,如果已存在,说明是已经加载过的模块,不再重复加载直接返回。
  • 在当前环境表中查找 loaders变量,这里存放的是所有加载器组成的数组 。 在 Lua代码中,有4个loader :
    1
    2
    static const lua_CFunction loaders[] =
    {loader_preload, loader_Lua, loader_C, loader_Croot, NULL};
    加载时,会依次调用 loaders数组中的四种 loader 。 如果加载的结果在Lua找中返回的是函数(前面提过,分析完Lua源代码文件,返回的是Closure ),那么说明加载成功,不再继续往下调用其他的 loader加载模块 。
    最后,调用lua call函数尝试加载该模块。 加载之前,在L回校中压入一个哨兵值sentinel,如果加载完毕之后这个值没有被改动过,则说明加载完毕,将registry [ ”_LOADED”]赋值为true表示加载成功 。

模块的热更新原理

能很好地支持代码热更新机制,是开发时选择使用脚本语言的原因之一 。 热更新的好处很在于,能在不重启程序或者发布新版本的情况下更新脚本,给调试和线上解决问题带来很大的便利,对开发效率有很大的提升 。
下面就来谈谈如何实现热更新 。先简单回顾之前提过的模块和lrequire机制 。 Lua内部提供了一个require 函数来实现模块的加载,它做的事情主要有以下几个。

  • 在registry [二LOADED”]表中判断该模块是否已经加载过了,如果是则返回,避免重复加载某个模块代码 。
  • 依次调用注册的 loader来加载模块 。
  • 将加载过的模块赋值给registry [”一LOADED "]表。
    而如果要实现Lua的代码热更新,其实也就是需要重新加载某个模块,因此就要想办法让Lua虚拟机认为它之前没有加载过。 查看Lua代码可以发现 , registry [”一LOADED ”]表实际上对应的是package.loaded表,这在以下函数中有体现:
    1
    2
    3
    4
    5
    6
    LUALIB_API int luaopen_package (lua_State *L) {
    int i;
    /* create new type _LOADLIB */
    luaL_newmetatable(L, "_LOADLIB");
    lua_pushcfunction(L, gctm);
    lua_setfield(L, -2, "__gc");
    因此,事情就很简单了,需要提供require_ex函数,可以把它理解为require的增强版 。 使用这个函数,可以动态更新某个模块的代码:
    1
    2
    3
    4
    5
    6
    7
    8
    干unction require_ex( _mname )
    print( string .format("require_ex =喃5 ”,_mname) )
    if package.loaded[_mname] then
    print( string.format(”require_ex module[ %s] reload”,_mname))
    end
    package .loaded[_mname] = nil
    require( _mname )
    end
    这个函数做的事情一目了然 。 首先,判断是否曾经加载过这个模块,如果有,则打印一条日志,表示需要重新加载某个模块,然后将该模块原来在表中注册的值赋空,然后再次调用require进行模块的加载和注册。

一般热更新都是函数的实现,所以需要对全局变量做一些保护 。 比如,当前某全局变量为 100 ,表示某个操作已经进行了 100次,它不能因为热更新重置为0 ,所以要对这些不能改变的全局变量做一个保护,最简单的方式就是这样 :

1
a = a or o

这个原理很简单,只有当前a这个变量没有初始值的时候才会赋值为0 ,而后面不管这个Lua文件被加载多少次, a者~J之会因为重新加载了Lua代码而发生改变 。

从lua-5.1.1中分离出来的lenv实现代码

lenv

reference

《lua设计与实现》
lua-5.1.1源码