技术说明 11

原文:https://www.lua.org/notes/ltn011.html

Technical Note 11 技术说明 11

Last update: Wed Feb 19 09:25:05 EST 2003

Require revisited: Import

by Wim Couwenberg

This LTN depends on “loadfile,” introduced in Lua 5.0

​ 此 LTN 依赖于 Lua 5.0 中引入的“loadfile”。

Abstract 本说明解释了基于 Lua 标记方法的多重继承样式类系统,该系统提供了类似于 Python 等语言的性能。

Lua 4.1 introduced the “require” function that loads and runs a file unless it already loaded. Lua 5.0 offers require as a built-in function in its base lib. The require command together with LTN 7 “Modules & packages” offers a basis for simple module support in Lua. This technical note proposes an improved version of require, dubbed “import.” The proposed import scheme avoids direct access to the globals, corrects a globals related security loophole and handles cyclic module dependencies gracefully.

​ Lua 4.1 引入了“require”函数,该函数加载并运行文件,除非该文件已加载。Lua 5.0 在其基本库中提供 require 作为内置函数。require 命令与 LTN 7“模块和包”一起为 Lua 中的简单模块支持提供了基础。此技术说明提出了一个改进版的 require,称为“import”。提议的 import 方案避免了对全局变量的直接访问,纠正了一个与全局变量相关的安全漏洞,并妥善处理了循环模块依赖关系。

The problem 问题

The module approach of LTN 7 proposes that a package should publish its public interface (wrapped in a table) in the globals table. This has the following drawbacks:

​ LTN 7 的模块方法建议包应在全局表中发布其公共接口(包装在一个表中)。这有以下缺点:

  • The global name used by the package might already hold data (a name clash.)
  • 包使用的全局名称可能已经保存了数据(名称冲突)。
  • Users of the package must know to which global name the package assigns its interface.
  • 包的用户必须知道包将其接口分配给哪个全局名称。
  • Metamethods set on the globals table might interfere with the loading of a module.
  • 在全局表上设置的元方法可能会干扰模块的加载。

The current implementation of require has also some shortcomings:

​ require 的当前实现也有一些缺点:

  • Require relies on the LTN 7 system of global public interfaces as described above and provides no further package management.
  • Require 依赖于上面描述的 LTN 7 全局公共接口系统,并且不提供进一步的包管理。
  • The require call in Lua 5.0 passes its own table of globals on to the required package, thereby offering the package a severe security loophole. Because require is a built-in C function, a “setglobals” call does not apply to require to prevent this.
  • Lua 5.0 中的 require 调用将其自己的全局表传递给所需的包,从而为该包提供严重的安全性漏洞。由于 require 是一个内置的 C 函数,因此“setglobals”调用不适用于 require 来防止这种情况。
  • If modules require each other (i.e. are cyclically dependendent) then a require call will recurse indefinitely, resulting in a stack overflow.
  • 如果模块相互需要(即循环依赖),则 require 调用将无限递归,从而导致​栈溢出。

The solution 解决方案

The proposed import scheme addresses the problems posed by global package names, globals security loopholes and cyclic dependencies. Import can be completely implemented in vanilla Lua 5. The main points:

​ 建议的导入方案解决了全局包名称、全局安全性漏洞和循环依赖带来的问题。Import 可以完全在 vanilla Lua 5 中实现。要点如下:

  • A package returns a “package install” function (PIF) that is in turn called by import.
  • 包返回一个“包安装”函数 (PIF),该函数反过来由 import 调用。
  • A table is passed to the PIF into which the package’s public interface should be inserted. This table is then returned as the result of the import call. A package should no longer install a global interface.
  • 将一个表传递给 PIF,包的公共接口应插入其中。然后将此表作为 import 调用的结果返回。包不再应该安装全局接口。
  • The package name and the full package path are passed as the second and third parameter to the PIF.
  • 包名称和完整包路径作为第二个和第三个参数传递给 PIF。
  • Import imposes its caller’s globals on the imported package. Import 将其调用者的全局变量强加于导入的包。
  • Import will report an error if a package is used before it is fully imported. This could happen during import of cyclic dependent packages. (A package is “used” if its public interface is accessed.) Packages can be cyclically dependendent without using each other during import. In this case import will succeed without error. (An example is given in section Explanation below.)
  • 如果在完全导入包之前使用该包,导入将报告错误。这可能发生在导入循环依赖包期间。(如果访问了包的公共接口,则该包被“使用”。)包可以循环依赖,而不会在导入期间互相使用。在这种情况下,导入将成功,不会出错。(下面解释部分给出了一个示例。)

The import function could be implemented with the following Lua 5.0 code.

​ 可以使用以下 Lua 5.0 代码实现导入函数。

local imported = {}

local function package_stub(name)
  local stub = {}
  local stub_meta = {
    __index = function(_, index)
      error(string.format("member `%s' is accessed before package `%s' is fully imported", index, name))
    end,
    __newindex = function(_, index, _)
      error(string.format("member `%s' is assigned a value before package `%s' is fully imported", index, name))
    end,
  }
  setmetatable(stub, stub_meta)
  return stub
end

local function locate(name)
  local path = LUA_PATH
  if type(path) ~= "string" then
    path = os.getenv "LUA_PATH" or "./?.lua"
  end
  for path in string.gfind(path, "[^;]+") do
    path = string.gsub(path, "?", name)
    local chunk = loadfile(path)
    if chunk then return chunk, path end
  end
  return nil, path
end

function import(name)
  local package = imported[name]
  if package then return package end
  local chunk, path = locate(name)
  if not chunk then
    error(string.format("could not locate package `%s' in `%s'", name, path))
  end
  package = package_stub(name)
  imported[name] = package
  setglobals(chunk, getglobals(2))
  chunk = chunk()
  setmetatable(package, nil)
  if type(chunk) == "function" then
    chunk(package, name, path)
  end
  return package
end

Typical use of import is as follows:

​ 导入的典型用法如下:

-- import the complex package
local complex = import "complex"

-- complex now holds the public interface
local x = 5 + 3*complex.I

A package should be structured as follows:

​ 包应按如下方式构建:

-- first import all other required packages.
local a = import "a"
local b = import "b"

-- then define the package install function.
-- the PIF more or less contains the code of a
-- LTN 7 package.
local function pif(Public, path)

local Private = {}

function Public.fun()
  -- public function
end

-- etc.
end

-- return the package install function
return pif

Explanation 解释

Setting a “package stub” just before the package is loaded must trap any access to the stub (invoked by a nested import.) In order for this to work, additional imports should be placed in the global scope of each package involved, typically as the first calls. Note that the stub (stripped from its access restrictions) will later hold the package’s public interface. In particular it is safe to refer to an imported interface (e.g. through upvalues) even in cyclic dependencies, as long as the interface is not actually accessed.

​ 在加载包之前设置“包存根”必须捕获对存根的任何访问(由嵌套导入调用)。为了实现此目的,应将其他导入放在每个相关包的全局作用域中,通常作为第一个调用。请注意,存根(已去除其访问限制)稍后将保存包的公共接口。特别是,即使存在循环依赖关系,只要实际上未访问该接口,就可以安全地引用导入的接口(例如,通过向上值)。

Import is almost backward compatible with require. Import will however not define the _REQUIREDNAME global during loading. An “old style” package that does not return a PIF will still be loaded and run but import returns an empty public interface. This will not impact old style code because require has no return values.

​ Import 与 require 几乎向后兼容。但是,Import 在加载期间不会定义 _REQUIREDNAME 全局变量。仍将加载并运行不返回 PIF 的“旧式”包,但 import 返回一个空的公共接口。这不会影响旧式代码,因为 require 没有返回值。

Here is an example of two packages mutually importing each other. Because neither one actually uses the other during import, this will not be a problem.

​ 以下是一个两个包相互导入的示例。因为在导入期间它们彼此之间实际上都不使用对方,所以这不会成为问题。

Package “a.lua”:

​ 包“ a.lua ”:

local b = import "b"

local function pif(pub, name, path)

function pub.show()
  -- use a message from package b
  print("in " .. name .. ": " .. b.message)
end

pub.message = "this is package " .. name .. " at " .. path

end

return pif

Package “b.lua”:

​ 包“ b.lua ”:

local a = import "a"

local function pif(pub, name, path)

function pub.show()
  -- use a message from package a
  print("in " .. name .. ": " .. a.message)
end

pub.message = "this is package " .. name .. " at " .. path

end

return pif

And some code importing and running both:

​ 以及导入并运行二者的代码:

local a = import "a"
local b = import "b"

a.show() -- prints "in a: this is package b at ./b.lua"
b.show() -- prints "in b: this is package a at ./a.lua"

Weaknesses 弱点

The import function assumes that the packages it imports are “well-behaved.” A package can of course still access and update the globals so care should be taken. Proper structuring of a package (import calls in its global scope, return a PIF, etc.) is not enforced.

​ import 函数假定它导入的包是“行为良好的”。当然,包仍然可以访问并更新全局变量,因此应小心。不会强制执行包的正确结构(在其全局作用域中导入调用、返回 PIF 等)。

Conclusion 结论

The require function has proved itself to be very useful. The proposed import scheme builds on this success. It provides more controlled package visibility and supports cyclic dependencies whenever possible. The import functionality is lightweight and can be completely defined in vanilla Lua 5.

​ require 函数已被证明非常有用。提议的 import 方案建立在此成功之上。它提供了更受控的包可见性,并在任何可能的情况下支持循环依赖。import 功能很轻巧,并且可以在纯净的 Lua 5 中完全定义。

最后修改 January 31, 2024: 更新 (e0896df)