Lua 无法判断何时何地引入了全局变量。
在值是函数的特殊情况下,debug.getinfo 可能会告诉您函数的定义位置(通常但并不总是与函数全局化的相同位置)来提供帮助。
您可以在引入全局时捕获所需的信息。这可以通过在全局表上使用__newindex 方法设置元表来完成。此方法将在引入新全局时调用(但不是在覆盖现有全局时)。在这种方法中,您可以通过debug.getinfo 找出调用者来自哪里。还要注意,如果您的任何其他代码试图在全局环境中使用元表,您必须很好地使用它。 (它只能有一个元表。)
您也可以避免使用全局表。这样做的一种中间方法是覆盖环境。在 Lua 5.2 和 Lua 5.3 中,这是通过声明一个名为 _ENV 的本地表来完成的——所有对全局表的访问都将访问这个表。 (实际上,全局访问始终使用_ENV,而_ENV 默认为_G。)您可以通过为_ENV 提供一个将访问转发到_G(或任何其他环境)的元表来使其几乎不可见。这里的区别在于,即使_G 中存在绑定,__newindex 仍然会被调用,因此该方法可以检测覆盖。
使用_ENV,虽然本质上是一个范围的本地(例如,每个文件都需要覆盖它)。不过,这样的钩子也可以全局安装。如果您使用 load 函数手动加载模块(不太可能),您可以提供自定义 _ENV 作为参数。如果您使用require,则可以通过覆盖(或猴子修补)package.searchers[2] 中的 Lua 搜索器在执行加载文件之前获取它的保留。这是require 调用的内置函数,用于在文件系统中查找文件然后加载它。返回值是 require 然后运行的加载函数。因此,在加载之后但在返回到require 之前,您可以使用debug.setupvalue 覆盖默认的_ENV 值(如果有)。
示例代码(仅经过轻微测试):
local global_info = {}
local default_searcher2 = package.searchers[2]
package.searchers[2] = function(...)
local result = default_searcher2(...)
local parent_environment = _G
local my_env = setmetatable({}, {
__index = parent_environment,
__newindex = function(self, k, v)
local new_info = debug.getinfo(2)
-- keeping rich data like this could be a memory leak
-- if some globals are assigned repeatedly, but that
-- may still be okay in a debugging scenario
local history = global_info[k]
if history == nil then
history = {}
global_info[k] = history
end
table.insert(history, {info = new_info, value = v})
parent_environment[k] = v
end,
})
if type(result) == "function" then
debug.setupvalue(result, 1, my_env)
end
return result
end
function gethistory(name)
local history = global_info[name]
if history == nil then
print('"' .. name .. '" has never been defined...')
else
print('History for "' .. name .. '":')
for _, record in ipairs(history) do
print(record.info.short_src .. ": " .. record.info.currentline)
end
end
end
请注意,这里的钩子仅适用于运行此代码后所需的文件,并且基本上仅适用于通过内置 require 包含的 Lua 文件(不是 C 库)。它不会在全局环境中设置元表,因此不会在那里发生冲突,但如果文件直接访问_G(或者例如在他们自己的_ENV 表中设置对_G 而不是_ENV 的访问权限,则可以绕过它)。这样的事情也可以考虑,但它可能是一个兔子洞,这取决于你需要这个补丁有多“不可见”。
在 Lua 5.1 中,您使用 setfenv 而不是 _ENV,我相信它可以用于类似的效果。
还要注意,我概述的所有方法只能检测在运行时实际执行的全局访问。