【问题标题】:Introspecting _ENV from coroutines从协程内省 _ENV
【发布时间】:2019-07-23 09:48:20
【问题描述】:

注意:我使用的是 Lua 5.3 版。


这个问题是由Lua 编程(第 4 版)的练习 25.1(第 264 页)引起的。该练习内容如下:

练习 25.1: 调整 getvarvalue(清单 25.1)以使用不同的协程(如 debug 库中的函数)。

练习引用的函数getvarvalue逐字复制如下。

-- Listing 25.1 (p. 256) of *Programming in Lua* (4th ed.)

function getvarvalue (name, level, isenv)
  local value
  local found = false

  level = (level or 1) + 1

  -- try local variables
  for i = 1, math.huge do
    local n, v = debug.getlocal(level, i)
    if not n then break end
    if n == name then
      value = v
      found = true
    end
  end
  if found then return "local", value end

  -- try non-local variables
  local func = debug.getinfo(level, "f").func
  for i = 1, math.huge do
    local n, v = debug.getupvalue(func, i)
    if not n then break end
    if n == name then return "upvalue", v end
  end

  if isenv then return "noenv" end   -- avoid loop

  -- not found; get value from the environment
  local _, env = getvarvalue("_ENV", level, true)
  if env then
    return "global", env[name]
  else        -- no _ENV available
    return "noenv"
  end

end

下面是我对该函数的增强版,它实现了练习中指定的附加功能。这个版本接受一个可选的thread 参数,预计是一个协程。此增强版与原始getvarvalue 之间的唯一区别是:

  1. 额外可选thread参数的处理;
  2. level参数的特殊设置取决于thread参数是否与正在运行的协程相同;和
  3. 在对debug.getlocaldebug.getinfo 的调用以及递归调用中传递thread 参数。

(我在源代码中通过编号的cmets标记了这些差异。)

function getvarvalue_enhanced (thread, name, level, isenv)
  -- 1
  if type(thread) ~= "thread" then
    -- (thread, name,  level, isenv)
    -- (name,   level, isenv)
    isenv = level
    level = name
    name = thread
    thread = coroutine.running()
  end

  local value
  local found = false

  -- 2
  level = level or 1
  if thread == coroutine.running() then
    level = level + 1
  end

  -- try local variables
  for i = 1, math.huge do
    local n, v = debug.getlocal(thread, level, i) -- 3
    if not n then break end
    if n == name then
      value = v
      found = true
    end
  end
  if found then return "local", value end

  -- try non-local variables
  local func = debug.getinfo(thread, level, "f").func  -- 3
  for i = 1, math.huge do
    local n, v = debug.getupvalue(func, i)
    if not n then break end
    if n == name then return "upvalue", v end
  end

  if isenv then return "noenv" end   -- avoid loop

  -- not found; get value from the environment
  local _, env = getvarvalue_enhanced(thread, "_ENV", level, true)  -- 3
  if env then
    return "global", env[name]
  else
    return "noenv"
  end

end

这个函数工作得相当好,但我发现了一个奇怪的情况1它失败了。下面的函数make_nasty生成一个协程,getvarvalue_enhanced找不到_ENV变量;即它返回"noenv"。 (作为nasty 基础的函数是闭包closure_B,它又调用闭包closure_A。然后产生的是closure_A。)

function make_nasty ()
  local function closure_A () coroutine.yield() end
  local function closure_B ()
    closure_A()
  end

  local thread = coroutine.create(closure_B)
  coroutine.resume(thread)
  return thread
end

nasty = make_nasty()
print(getvarvalue_enhanced(nasty, "_ENV", 2))
-- noenv

相比之下,几乎相同的函数make_nice 生成一个协程,getvarvalue_enhanced 成功地为其找到了一个_ENV 变量。

function make_nice ()
  local function closure_A () coroutine.yield() end
  local function closure_B ()
    local _ = one_very_much_non_existent_global_variable  -- only difference!
    closure_A()
  end

  local thread = coroutine.create(closure_B)
  coroutine.resume(thread)
  return thread
end

nice = make_nice()
print(getvarvalue_enhanced(nice, "_ENV", 2))
-- upvalue  table: 0x558a2633c930

make_nastymake_nice 之间的唯一区别在于,在后者中,闭包 closure_B 引用了一个不存在的全局变量(并且对它没有任何作用)。

问:如何修改getvarvalue_enhanced,使其能够像nice 那样为nasty 定位_ENV


编辑:更改了 make_nastymake_nice 中的闭包名称。


EDIT2:练习 25.3(同一页)的措辞可能与此处相关(我的重点):

练习 25.3: 编写一个版本的 getvarvalue(清单 25.1),它返回一个表格,其中包含所有变量在调用函数中可见。 (返回的表不应包含环境变量;而应从原始环境继承它们。)

这个问题意味着应该有一种方法可以从函数中获取仅可见的变量,函数是否使用它们。这些变量肯定包括_ENV。 (作者是 Lua 的创造者之一,所以他知道他在说什么。)


1 我相信,对这个例子中发生的事情有更好理解的人将能够想出一种不那么复杂的方法来引发相同的行为。我在这里展示的示例是我偶然发现的情况的最小形式。

【问题讨论】:

  • 我会说这是安全的:不要在生产中使用这样的代码。一旦您在没有调试符号的情况下预编译代码,它就会中断。它只能用于临时调试或可选调试组件。
  • @DarkWiiPlayer:谢谢你的建议。要清楚,您指的是哪个代码?帖子里的所有代码?
  • 一般情况下,不要依赖debug,但更准确地说,如果在使用luac 编译时去掉调试符号,所有依赖变量名的东西都会完全中断。只有当您可以控制使用的代码时,它才真正可行,因此在编写要发布的库时根本不可行。

标签: debugging reflection lua coroutine introspection


【解决方案1】:
  local function inner_closure () coroutine.yield() end
  local function outer_closure ()
    inner_closure()
  end

下面的函数make_nasty 生成一个协程,getvarvalue_enhanced 找不到_ENV 变量;即它返回“noenv”

这是正确的行为。
闭包outer_closure 有upvalue inner_closure 但没有upvalue _ENV
这就是词法作用域的工作原理。
一些闭包没有_ENV upvalue 也没关系。

在您的示例中,闭包 inner_closure 未在 outer_closure 的主体内定义。
inner_closure 未嵌套在 outer_closure 中。

【讨论】:

  • 谢谢,但您的回答完全没有解决我在帖子末尾实际提出的问题。
  • @kjo - 您可以将行 _ENV=_ENV 显式插入到 outer_closure 中,这样它肯定具有 _ENV 上值。如果您无法修改outer_closure,那么您将无法获得它的_ENV,因为它没有它。但它有一个上值确实有上值_ENV,所以你可能会尝试搜索整个依赖树:-)
【解决方案2】:

这是不可能的。

如果闭包不使用全局环境_ENV,那么它就没有任何升值。

类似的函数

local something = 20

local function noupval(x, y)
   return x * y
end

不需要或有任何升值,甚至对全球环境也没有。


这个问题意味着应该有一种方法可以获取仅在函数中可见的变量,无论函数是否使用它们。

确实没有。您可以通过查看luac -p -l <your_code.lua> 的输出轻松确认这一点,更准确地说是查看每个函数的上值。

如果有的话,我认为使用 visible 这个词有点误导。可见性只在创建闭包时很重要,但一旦它被关闭,该闭包就只有一组可以访问的上值。


练习 25.3:编写一个 getvarvalue 版本(清单 25.1),它返回一个表,其中包含在调用函数中可见的所有变量。 (返回的表不应包含环境变量;而应从原始环境继承它们。)

您可能误解了该练习;我的理解是这样的:

local upvalue = 20
local function foo()
  local var = upvalue -- Create 1 local and access 1 upvalue
  type(print) == "function" -- Access _ENV so it becomes an upvalue
  return getvarvalue_enhanced()
end

以上将返回{var = 20, upvalue = 20, _ENV = <Proxy table to _ENV>}

毕竟,它专门询问调用函数,而不是你作为参数传递的函数。

这并没有改变这样一个事实,即如果您访问它,您仍然只能获得_ENV。如果您不使用任何全局变量,则该函数将不会引用 _ENV

【讨论】:

  • 感谢您的意见。不过,您的noupval 示例有点误导/混淆,因为毕竟,无论是否使用它,都没有在它之外定义任何上值。如果您在noupval 的定义之前添加local something = ...,您的观点会更清楚(至少对我而言)。
  • 请看我的EDIT2;它可能与您的答案有关。
  • @kjo 我扩大了我的答案;)
  • @DarkWiiPlayer - {var = 20, upvalue = 20, _ENV = <Global Environment>} 可能这不是我们所要求的。将_ENV 放在表中是不够的。 “继承环境变量”意味着为这种继承创建一个元表。
  • @EgorSkriptunoff 是的,我似乎第一次误解了这一点。无论哪种方式,实现它都是一个与 OP 要求的完全不同的问题,所以我不会通过解释如何做到这一点来扩展我的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-04-11
  • 2010-11-19
  • 1970-01-01
  • 1970-01-01
  • 2017-06-03
  • 2014-07-29
  • 2018-11-03
相关资源
最近更新 更多