Lua 將所有全局變量存放在一張普通表中,這張表稱為**環境**。理解環境機制是掌握 Lua 作用域、模塊封裝、沙箱安全、動態代碼執行的鑰匙。本文系統講解:全局環境 _G、_ENV 上值 (5.2+)、setfenv/getfenv (5.1)、元表攔截全局訪問、函數環境繼承、沙箱構建、模塊環境隔離實戰。
a = 1 ⇔ _G.a = 1 (5.1) / _ENV.a = 1 (5.2+)。_G 是全局環境的**自身引用** (_G._G == _G);僅 5.1 保證存在,5.2+ 為兼容保留。| 功能 | Lua 5.1 | Lua 5.2+ (5.3/5.4) |
|---|---|---|
| 獲取函數環境 | getfenv(f) | debug.getupvalue(f, 1) (第一個上值即 _ENV) |
| 設置函數環境 | setfenv(f, env) | debug.setupvalue(f, 1, env) 或 load(code, nil, "t", env) |
| 當前塊環境 | getfenv(0) | _ENV (語法層面的上值名) |
| 模塊環境隔離 | setfenv(1, M) | local _ENV = M (頂層局部) |
| 沙箱構建 | setfenv(sandbox_fn, safe_env) | load(code, nil, "t", safe_env) |
-- 嚴格模式:禁止未聲明全局讀寫
local declared = {}
setmetatable(_G, {
__newindex = function(t, k, v)
if not declared[k] then
error("Attempt to write undeclared global: " .. k, 2)
end
rawset(t, k, v)
end,
__index = function(t, k)
if not declared[k] then
error("Attempt to read undeclared global: " .. k, 2)
end
return rawget(t, k)
end
})
-- 聲明全局變量
function declare(name, initval)
declared[name] = true
rawset(_G, name, initval)
end
declare("CONFIG", {debug = true})
print(CONFIG.debug) -- true
-- print(UNDECLARED) -- ERROR
-- UNDECLARED = 1 -- ERROR
關鍵:用 rawset/rawget 繞過元方法;declared 表記錄合法全局名。
_ENV)。_ENV 後定義的函數,共享新環境。local a = 1 -- 在舊 _ENV (即 _G) 中
local newEnv = setmetatable({}, {__index = _G}) -- 繼承全局
_ENV = newEnv -- 當前塊環境切換
print(a) -- 1 (通過 __index 找到 _G.a)
b = 2 -- 寫入 newEnv.b
print(b) -- 2
print(_G.b) -- nil (未污染 _G)
local function foo() print(b) end -- 捕獲 newEnv
foo() -- 2
-- 再次切換
_ENV = setmetatable({}, {__index = newEnv})
c = 3
print(c) -- 3
foo() -- 仍為 2 (foo 捕獲的是舊 newEnv)
function sandbox_run(untrusted_code, safe_globals, timeout_ms)
-- 1. 準備安全環境:僅暴露白名單 API
local safe_env = {
print = print, -- 或包裝限制輸出長度
math = {abs=math.abs, max=math.max, min=math.min}, -- 子集
string = {len=string.len, sub=string.sub, format=string.format},
table = {insert=table.insert, remove=table.remove, sort=table.sort, concat=table.concat},
pairs = pairs, ipairs = ipairs, next = next, type = type,
tostring = tostring, tonumber = tonumber,
assert = assert, error = error, pcall = pcall, xpcall = xpcall,
select = select, unpack = table.unpack,
_VERSION = _VERSION
}
-- 2. 繼承基礎庫 (可選)
setmetatable(safe_env, {__index = function(_, k) return nil end}) -- 禁止訪問未白名單
-- 3. 編譯代碼 (Lua 5.2+ load 第四參數指定環境)
local chunk, err = load(untrusted_code, "sandbox", "t", safe_env)
if not chunk then return nil, "Compile error: " .. err end
-- 4. 執行 (可配合 debug.sethook 限制指令數實現超時)
if timeout_ms then
debug.sethook(function() error("Timeout") end, "", math.max(1, timeout_ms * 1000))
end
local ok, result = pcall(chunk)
debug.sethook() -- 取消鉤子
if not ok then return nil, "Runtime error: " .. result end
return result
end
-- 測試
local code = [[
local x = math.max(1, 2, 3)
print("max:", x)
os.exit(0) -- 嘗試調用危險函數
]]
local ok, err = sandbox_run(code)
print(ok, err) -- nil "Runtime error: ... attempt to call a nil value (global 'os')"
-- mylib.lua
local M = {}
_ENV = M -- 1. 切換當前塊環境為 M
-- 2. 所有「全局」定義自動進 M
local function private_helper() return 42 end -- 真正局部 (upvalue),外部不可見
function public_api(x)
return private_helper() + x
end
CONSTANT = 3.14
-- 3. 可選:暴露模塊元信息
_NAME = "mylib"
_VERSION = "1.0"
-- 4. 返回模塊表
return M
效果:
require "mylib" 不創建任何全局變量。public_api(),無需 M. 前綴。private_helper 為 Lua 上值,**不在 M 中**,絕對不可見。-- 運行多個隔離的「應用」在同一 Lua 狀態機
local apps = {}
function create_app(name)
local env = {_G = _G} -- 可選:受控訪問真全局
setmetatable(env, {__index = _G})
local app = {env = env}
apps[name] = app
return app
end
function run_in_app(app, code)
return load(code, "="..app.name, "t", app.env)()
end
local app1 = create_app("app1")
run_in_app(app1, "x = 10; print('app1 x=', x)")
local app2 = create_app("app2")
run_in_app(app2, "x = 20; print('app2 x=', x)")
-- app1.x=10, app2.x=20 互不干擾
| 誤區 | 正確認知 |
|---|---|
_G 就是全局環境,不可改 | _G 只是約定的全局引用,可重新賦值 (不推薦);5.2+ 核心是 _ENV |
setfenv 影響所有函數 | 只影響**指定函數** (或數字層級) 的環境,不改變調用者環境 |
模塊頂層 local M = {}; _ENV = M 後無法訪問標準庫 | 用 local _ENV = setmetatable({}, {__index = _G}) 繼承全局,或在 _ENV 切換前 local print = print 等上值捕獲 |
沙箱用 setmetatable(env, {__index=_G}) 就安全 | **極其危險**!惡意代碼可通過 getmetatable(env).__index 逃逸;必須**白名單**構建 env |
| 環境隔離會降低性能 | 現代 Lua 實現中環境查找極快 (上值直接索引);模塊隔離**零開銷** |
setfenv/getfenv,5.2+ 用 _ENV / load(..., env) / debug.setupvalue。local _ENV = M (繼承 _G 或白名單) —— 零污染、可重命名、私有上值。load(code, nil, "t", env) + 指令計數超時 —— 安全運行不受信代碼。「環境機制是 Lua 靈活性的核心,善用它,讓代碼結構清晰、安全邊界分明。」