相信大家在lua中都用过require。为了达到代码复用和结构化的目的,各种语言都有require机制。lua的require看似简单,其实里面有很多玄机。

一  require从哪里加载模块文件

从虚拟机的path,cpath等全局变量中。虚拟机有默认的值,在变量package.path和package.cpath中。例如我打印的path,cpath分别为:

path : /usr/local/share/lua/5.3/?.lua;/usr/local/share/lua/5.3/?/init.lua;/usr/local/lib/lua/5.3/?.lua;/usr/local/lib/lua/5.3/?/init.lua;./?.lua;./?/init.lua

cpath : /usr/local/lib/lua/5.3/?.so;/usr/local/lib/lua/5.3/loadall.so;./?.so

当然我们也可以改变默认的值。

我们可以看出上述的path其实并不是真正意义上的路径,他只是寻找lua文件的路径模版。最终还是要从上述的path中通过变换路径找到lua文件,这个就涉及到查找策略了。

二  require加载模块文件的策略

如果模块文件曾经被加载过,我们不会傻傻的再加载一次模块。所以加载模块要遵循一定的规则,也就是策略。这个策略是:

1 首先查找package.loaded表,检测是否被加载过。如果被加载过,require返回保存的值,这个值在哪里被保存后面会讲到。否则进行下面的为模块寻找加载器。

2 加载器在package的searchers表中,共有四个加载器。require也按照顺序来执行加载器。找到了就成功返回,没有就继续查找。

a   预加载器,执行package.preload[modname],一些特殊模块会有预加载器。

b  lua加载器查找前面介绍的package.path

c  c加载器查找package.cpath

d  一体化加载器,这个在后面会有详细解释。

三 加载器如何加载

由于路径是一个包含有一系列以;分隔的模版构成的字符串,所以在加载器加载文件时,首先将每个路径模版用模块名字替换其中的?,然后尝试打开这个文件名。例如,如果路径字符串是

"./?.lua;./?.lc;/usr/local/?/init.lua"

require('foo'),将会依次尝试打开文件./foo.lua,./foo.lc,以及/usr/local/foo/init.lua。如果模块名包含.,例如require('foo.a')那么将会依次尝试打开文件./foo/a.lua,./foo/a.lc,以及/usr/local/foo/a/init.lua。

同理,如果这一步没有找到,则以同样的方式查找cpath。例如cpath是这个字符串

 

"./?.so;./?.dll;/usr/local/?/init.so"

查找器查找模块 foo 会依次尝试打开文件 ./foo.so,./foo.dll, 以及 /usr/local/foo/init.so。 一旦它找到一个 C 库, 查找器首先使用动态链接机制连接该库。然后尝试在该库中找到可以用作加载器的 C 函数。 这个 C 函数的名字必须是 "luaopen_" 紧接模块名的字符串,其中字符串中所有的下划线都会被替换成点。 此外,如果模块名中有横线, 横线后面的部分(包括横线)都被去掉。 例如,如果模块名为 a.b.c-v2.1, 函数名就是 luaopen_a_b_c。

也就是不仅提供的c模块名字要和require的模块名字一致,而且c模块导出的函数也要符合规范才行。如果有该模块而函数名对不上则会出现如下错误

lua loader error : error loading module 'xxx' from file './luaclib/xxx.so'。

最后一个搜索器是一体化加载器。本质上他是cpath加载器的延伸。他允许多个导出函数绑定在一个c库里。例如require('foo.a'),上述加载器都没有加载到,那么试图加载foo模块(过程和上面一样),然后再在foo里寻找luaopen_foo_a导出函数。

lua为我们提供了在指定path中搜索模块的函数,package.searchpath(),过程上述已讲。

四 加载成功后

如果require('mod')成功,则在package.loaded['mod']中记录该模块。如果该模块没有返回值则package.loaded['mod']==true,有返回值则记录的是其返回值。

值得注意的是,在同一个lua虚拟机中,多次require同一个模块,该模块返回一个table,那么任何地方修改该table的值都会引起其他地方table值的改变。例如如下代码:

--mode_a.lua
local skynet = {
	name = 'shonm'
}

return skynet

-----------
--mode_b.lua
local skynet = require('mode_a')
skynet.age = 21
skynet.name = 'zxm'
return skynet


-----------
--test.lua

local skynet = require("mode_a")  
require('mode_b')
print(skynet.name)      --已经被改变为'zxm'

require的代码始终只会被执行一次,把上面的代码稍作改变:

--mode_a.lua
local skynet = {
	name = 'shonm'
}
skynet.name = 'tcj'
 
return skynet
 
-----------
--mode_b.lua
local skynet = require('mode_a')
skynet.age = 21
skynet.name = 'zxm'
return skynet
 
 
-----------
--test.lua

require('mode_b') 
local skynet = require("mode_a")     --再次加载时,不会执行mode_a的代码,因为上面已经执行过,只从package.loaded['mode_a']中获得table的值

print(skynet.name)      --仍然是'zxm'

注意,文中提到的package是lua暴露的一个全局模块,他是一个表,正如require,print一样,只不过他们是函数。类似模块还有os,math等待。

 

我自己写一个了蹩脚的lua解释器,能够执行大部分lua语法,代码在git上,赏颗星星

https://github.com/shonm520/mlua

 

欢迎加入QQ群 858791125 讨论skynet,游戏后台开发,lua脚本语言等问题。

 

参考:

http://www.runoob.com/manual/lua53doc/manual.html#pdf-package.searchpath

 

 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐