Lua 技術備忘 11

重新檢視 Require:Import

作者:Wim Couwenberg

此 LTN 依賴於 Lua 5.0 中推出的「loadfile」

摘要

Lua 4.1 推出了「require」函式,用於載入和執行檔案,除非該檔案已載入。Lua 5.0 在其基礎函式庫中提供 require 作為內建函式。require 指令搭配 LTN 7「模組和套件」,為 Lua 中的簡單模組支援提供基礎。此技術備忘提出 require 的改良版本,稱為「import」。建議的 import 架構避免直接存取全域變數,修正與全域變數相關的安全漏洞,並優雅地處理循環模組相依性。

問題

LTN 7 的模組方法建議套件應在其全域變數表格中公開其公開介面(包裝在表格中)。這有以下缺點 require 的目前實作也有一些缺點

解決方案

建議的 import 架構解決了全域套件名稱全域安全漏洞循環相依性所帶來的問題。Import 可以完全在香草 Lua 5 中實作。重點如下

import 函式可以使用以下 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

import 的典型用法如下

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

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

一個套件的結構應如下

-- 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

說明

在載入套件之前設定一個「套件 stub」,必須攔截對 stub 的任何存取(由巢狀 import 呼叫)。為了讓此機制運作,應將其他 import 放置在每個相關套件的全域範圍中,通常作為第一個呼叫。請注意,stub(已移除其存取限制)稍後會保留套件的公開介面。特別是,即使在循環依賴中,也可以安全地參照匯入的介面(例如透過 upvalues),只要實際上未存取該介面即可。

Import 幾乎與 require 向後相容。不過,import 在載入期間不會定義 _REQUIREDNAME 全域變數。不會傳回 PIF 的「舊式」套件仍會載入並執行,但 import 會傳回一個空的公開介面。這不會影響舊式程式碼,因為 require 沒有傳回值。

以下是兩個套件互相匯入的範例。由於在匯入期間,兩者都不會實際使用對方,因此這不會造成問題。

套件「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

套件「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

以及匯入並執行兩者的程式碼

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"

缺點

import 函式假設它匯入的套件「行為良好」。當然,套件仍可以存取並更新全域變數,因此應小心處理。套件的適當結構(在其全域範圍中進行 import 呼叫、傳回 PIF 等)並未強制執行。

結論

require 函式已證明非常有用。建議的 import 架構建立在此成功之上。它提供更受控的套件可見性,並在可能的情況下支援循環依賴。import 功能輕巧,且可以在純粹的 Lua 5 中完全定義。


最後更新:2003 年 2 月 19 日星期三上午 09:25:05 美東時間