cattenlinger 发表于 2026-8-7 22:00:21

【LUA】require 是如何工作的

本帖最后由 cattenlinger 于 2026-8-21 01:58 编辑


`require` 的工作方式和 Lua 的标准全局变量 `package` 有很强的关系。 `package` 是 `require` 函数对外曝露的信息与可配置项。关于 `package` 的说明会在后面补充。

从宏观流程上来说,`require ` 加载模块的步骤可以分为以下:
1. 搜索
2. 加载
3. 缓存并返回

搜索
---

`require` 会按顺序调用各个搜索器(searcher)来寻找模块。

```lua
--- modname:模块名
--- module:如果成功,返回所加载的内容,如果失败则为 nil
--- errmsg:第二个返回值,失败时返回,错误信息,通常是搜索过的路径
local module, errmsg = searcher(modname)
```

在 `require` 中,如果所有搜索器都没有返回模块,则会直接通过 `error()` 抛出错误。

搜索器
---
搜索器总共有四个:
1. preload:预设加载器,它排在最前
2. path:搜索 `package.path`(也就是 LUA_PATH)的搜索器
3. cpath:搜索 `package.cpath`(也就是 LUA_CPATH)的搜索器
4. cpath (sub-module):cpath 子模块搜索器,专门针对 C 模块中的子模块。

预加载在本质上是直接查询 `package.preload` 中的加载器。这个表中默认没有内容,它通常是用来修改指定模块的加载方式:例如说一个被静态到 Lua 解释器中的模块,那么可以将它的模块名提前放到 `package.preload` 中,对应一个加载器(loader),调用时返回一个模块。

path 搜索会将所有的 `.` 换成系统的路径分隔符,替换掉 LUA_PATH 中的 `?`,以 `;` 为分割,在路径列表里一个个地搜索。

这个路径的搜索函数是通用的,就是 `package.searchpath`,它传入两个字符串参数:1. 模块名,2. 路径列表。如果找到,则返回最终的文件名,如果找不到,则返回 nil 以及错误信息,一般为搜索过的路径历史。

例如搜索路径如下:
```
> package.path
./?.lua;./?/init.lua;/opt/homebrew/share/lua/5.4/?.lua;/opt/homebrew/share/lua/5.4/?/init.lua;/opt/homebrew/lib/lua/5.4/?.lua;/opt/homebrew/lib/lua/5.4/?/init.lua
```
结果:
```
> package.searchpath('cqueues', package.path)
/opt/homebrew/luarocks/share/lua/5.4/cqueues.lua
```

cpath 搜索与 path 搜索流程基本一致。在找到对应的 C 动态库后,lua 会拿出其中以 `luaopen_ + 模块名` 为名的函数。

加载
---

当 path 搜索找到文件之后,便调用 `loadfile()` 加载对应的文件,得到一个函数,调用并将返回值写入 `package.loaded` 后返回。这里有个小细节:如果它调用没有返回任何内容,对应的 key 值可能会被设置为 `true`,表示已经加载过了。

但因为 C 的函数名不能包含 `.`,在加载 C 语言子模块的时候,候补的 cpath 子模块搜索器会将第一个点之前的名称抽出,将模块名的 `.` 换成 `_`,再拼凑出新的函数名来搜索。

1. 假设 `local modname = 'cqueues.promise'`
2. 抽出主模块名 `local main_name = 'cqueues'`
3. 搜索 C 模块`local modpath = package.searchpath('cqueues', package.cpath)`,得到模块路径
4. 转换模块名,也就是得到 `cqueues_promise`
5. 加载 C 函数`local loader = loadlib(modpath, 'luaopen_cqueues_promise')`

调用得到的加载函数时,`require` 会将模块名作为第一个参数传入。

缓存
---
在 `package.loaded` 中存储着已经加载了的模块(module)。搜索的前置步骤会从这个表里找模块名。如果找到,便直接返回其中的模块,搜索流程不再会被触发。(这和表的 `__index` 元方法是同质的)

```lua
local module = loader(modname)
package.loaded = module
```

成功加载后,模块会被写入 `package.loaded` 中,下次再用相同的模块名调用 `require` 会因为 `package.loaded` 中已存在同名值,直接返回。所以,如果想要强制一个模块被重新加载,直接将其中对应的值设为 `nil` 即可。

模块名
---
模块名全小写是一种习惯,require 并没有对此作限制。而在搜索的时候,Lua 默认的文件系统搜索器也只是将 `.` 替换成配置好的路径分隔符。而当目的模块是 C 模块时,将子模块的 `.` 换成 `_` 并调用对应的 C 函数。

所以,模块名能用什么字符,取决于下层的搜索器的特性。

回到 `require` 函数
---

`require` 函数本质上就是个普通的 Lua 函数,辅助对 `loadfile` 和 `package.loadlib` 的调用。

在需要更复杂的加载逻辑时,通常有这些方式来实现:

1. 修改 `package.searchers` 的内容,写自己的搜索器。
2. 使用一个新的函数包裹 `require`,在最后才回落到标准的 require。
3. 重新写一个

重新写一个的场景,通常是因为要控制所加载的模块的全局变量表(但从 Lua 5.2 开始,没有全局,只有上值了)。

标准的 `require` 函数,其上值是启动时的 `_ENV`,也就是 `_G`,它已经在构造 require 的时候作为上值封入 require 中了。
我们拿到的只是对它的引用,如果想修改 require 加载时使用的 `_ENV`,这是办不到的(补充:这里不讨论使用调试组件来干预 Lua 虚拟机的情况)。

当然,对这个默认的 `_ENV` 设置元方法,把它改成一个代理表(proxy)也不是不行。

页: [1]
查看完整版本: 【LUA】require 是如何工作的