找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 13|回复: 0

【LUA】require 是如何工作的

[复制链接]
发表于 昨天 22:00 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
本帖最后由 cattenlinger 于 2026-8-7 22:00 编辑

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

从宏观流程上来说,require 加载模块的步骤可以分为以下:

  1. 搜索
  2. 加载
  3. 缓存并返回

搜索

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

--- 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 元方法是同质的)

local module = loader(modname)
package.loaded[modname] = module

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

模块名

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

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

回到 require 函数

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

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

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

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

标准的 require 函数,其上值是启动时的 _ENV,也就是 _G,它已经在构造 require 的时候作为上值封入 require 中了,我们拿到的只是对它的引用,这是无法修改的。当然,对这个默认的上值设置元方法,把它改成一个代理表(proxy)也不是不行。

回复

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2026-8-8 05:44 , Processed in 0.019467 second(s), 7 queries , Gzip On, Redis On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表