我可以使用 Lua 的要求来设置调用文件的环境吗?

Can I use Lua's require to set environment of the calling file?

有没有办法在 Lua 文件中调用 require,并让模块设置调用它的文件的环境?例如,如果我有一个定义函数 RootSequence 的 DSL(领域特定语言)定义在 table 中,我可以在模块中有类似 setfenv(1, dslEnv) 的东西吗这让我可以访问像全局变量这样的函数?

我的目标是使用行为树 DSL,使我的定义文件看起来像这样(或尽可能接近它):

require "behaviortrees"

return Root {
    Sequence {
        Leaf "leafname",
        Leaf "leafname"
    }
}

无需明确地将 RootSequenceLeaf 纳入范围,也不必限定 behaviortrees.Sequence.

等名称

简而言之,我试图使定义文件尽可能干净,没有任何多余的行使树定义混乱。

至少在Lua5.2中,_ENV是一个决定环境table的局部。您可以更改任何函数的环境,基本上是块。

_ENV = behaviortrees;

另一种方法是自动复制每个字段:

do
    _ENV = _ENV or _G;

    for k, v in next, behaviortrees do
        _ENV[k] = v;
    end
end

然而,手动本地化 behaviortrees 中的每个字段可能更有效。

模块"behaviortrees.lua"

local behaviortrees = {
   -- insert your code for these functions
   Root     = function(...) ... end,
   Sequence = function(...) ... end,
   Leaf     = function(...) ... end,
}

-- Now set the environment of the caller.  Two ways are available:

-- If you want to make DSL environment isolated from Lua globals
-- (for example, "require" and "print" functions will not be available 
--  after executing require "behaviortrees")
setfenv(3, behaviortrees)
-- or 
-- If you want to preserve all globals for DSL
setfenv(3, setmetatable(behaviortrees, {__index = getfenv(3)}))

主要Lua程序:

require "behaviortrees"

return Root {
   Sequence {
      Leaf "leafname",
      Leaf "leafname"
   }
}

我可以在模块中加入类似 setfenv(1, dslEnv) 的东西,让我可以访问像全局变量这样的函数吗?

当然可以。您只需要找出要使用的正确堆栈级别,而不是 setfenv 调用中的 1。通常你会使用带有 debug.getinfo 调用的循环在堆栈中向上走,直到你在堆栈上找到 require 函数,然后你再移动一些直到你找到下一个主要块(以防万一有人在函数中调用 require)。这是您必须使用 setfenv 的堆栈级别。但我可以建议 ...

不同的方法

require in Lua 是可插入的。您可以向 package.loaders 数组添加一个函数(称为搜索器),require 将在它尝试加载模块时调用它。假设您所有的 DSL 文件都有一个 .bt 后缀,而不是通常的 .lua。然后,您将使用普通 Lua 搜索器的重新实现,不同之处在于您将查找 .bt 文件而不是 .lua 文件,并且您将调用 setfenvloadfile 编写的函数 return。像这样:

local function Root( x ) return x end
local function Sequence( x ) return x end
local function Leaf( x ) return x end


local delim = package.config:match( "^(.-)\n" ):gsub( "%%", "%%%%" )

local function searchpath( name, path )
  local pname = name:gsub( "%.", delim ):gsub( "%%", "%%%%" )
  local msg = {}
  for subpath in path:gmatch( "[^;]+" ) do
    local fpath = subpath:gsub( "%?", pname ):gsub("%.lua$", ".bt") -- replace suffix
    local f = io.open( fpath, "r" )
    if f then
      f:close()
      return fpath
    end
    msg[ #msg+1 ] = "\n\tno file '"..fpath.."'"
  end
  return nil, table.concat( msg )
end


local function bt_searcher( modname )
  assert( type( modname ) == "string" )
  local filename, msg = searchpath( modname, package.path )
  if not filename then
    return msg
  end
  local env = { -- create custom environment
    Root = Root,
    Sequence = Sequence,
    Leaf = Leaf,
  }
  local mod, msg = loadfile( filename )
  if not mod then
    error( "error loading module '"..modname.."' from file '"..filename..
           "':\n\t"..msg, 0 )
  end
  setfenv( mod, env ) -- set custom environment
  return mod, filename
end


table.insert( package.loaders, bt_searcher )

如果你把它放在一个模块中,并且 require 它从你的主程序中一次,你可以 require 你的 DSL 文件和自定义环境从你所在的地方的 .bt 文件也会放置您的 .lua 文件。而且您甚至不需要 DSL 文件中的 require("behaviortrees")。例如:

文件xxx.bt:

return Root {
  Sequence {
    Leaf "leafname",
    Leaf "leafname"
  }
}

文件main.lua:

#!/usr/bin/lua5.1
require( "behaviortrees" ) -- loads the Lua module above and adds to package.loaders
print( require( "xxx" ) ) -- loads xxx.bt (but an xxx Lua module would still take precedence)