6.全局环境与沙盒
6.1 知识点
全局变量、环境表和沙盒
这篇主要聊聊Lua中 变量去哪找,以及怎么限制它去哪找。
全局变量是写进环境表;
_ENV 决定变量去哪找;
沙盒就是给 chunk 换一张受限制的环境表。
我们学习过,没写 local 的名字,会写进环境表,默认就是 _G。
roleName = "RoleA"
print(roleName) -- 输出:RoleA
print(_G.roleName) -- 输出:RoleA
给一段脚本换一张环境表,只放允许它用的名字,就是在做沙盒。
_G 减负
_G 可以挂少量框架入口,比如 Game、日志、模块管理器。但要集中,别什么变量函数都往全局一坨一坨的拉。
function OpenPanel()
print("Open Bag Panel")
end
function OpenPanel()
print("Open Shop Panel")
end
OpenPanel() -- 输出:Open Shop Panel
后面的定义会把前面的盖掉。两个模块各写了一个 OpenPanel,要是还在不同文件中覆盖查起来就蛋疼了。
优化的写法是挂一个入口:
_G.Game = _G.Game or {}
Game.UI = Game.UI or {}
function Game.UI.OpenPanel(panelName)
print("Open Panel:", panelName)
end
Game.UI.OpenPanel("Bag")
-- 输出:Open Panel: Bag
_ENV 决定变量去哪找
Lua 5.2 起(含 5.3、5.4),可以把 _ENV 认为是当前代码块查 free name 用的环境表。默认 _ENV == _G。
print(_ENV == _G) -- 输出:true(Lua 5.2+)
print(_G._G == _G) -- 输出:true
没写 local 的赋值和读取都是去 _ENV找:
roleName = "RoleA" -- 约等于 _ENV.roleName = "RoleA"
print(roleName) -- 约等于 _ENV.print(_ENV.roleName)
换环境就是在局部作用域里重写 _ENV:
local env = {
roleName = "RoleA",
print = print
}
local function Test()
local _ENV = env
print(roleName) -- 输出:RoleA
end
Test()
print 也要放进 env。换了 _ENV 以后,print 也会从新表里找:
local env = { roleName = "RoleA" }
local function Test()
local _ENV = env
print(roleName) -- 报错:attempt to call global 'print' (a nil value)
end
这里报的是 print 为 nil,roleName 是在 env 里存在的。
setfenv
5.1 / LuaJIT 改函数环境用 setfenv / getfenv。版本差异篇讲过细节,不赘述:
local env = {
roleName = "RoleA",
print = print
}
local function Test()
print(roleName)
end
setfenv(Test, env)
Test() -- 输出:RoleA
chunk 和 load
chunk 就是一段交给 Lua 加载并执行的代码。.lua 文件是 chunk,字符串也可以是 chunk。
load 可以把字符串加载成函数;和沙盒相关的只有最后一个参数:环境表。
local env = {
roleName = "RoleB",
print = print
}
local code = "print(roleName)"
-- load(chunk, chunkname, mode, env)
local func = load(code, "RoleChunk", "t", env)
func() -- 输出:RoleB
chunkname 只是命名一下,影响报错堆栈;mode 常用参数是:
| mode | 含义 |
|---|---|
"t" |
只允许文本 chunk;沙盒、不可信输入优先用这个 |
"b" |
只允许二进制 chunk |
"bt" |
两者都允许;load 默认值 |
沙盒场景一般不要传 "b" / "bt"。
5.1 / LuaJIT 没有 load 的第四个参数 env。字符串 chunk 先 loadstring 编译成函数,再 setfenv 换环境:
local env = {
roleName = "RoleB",
print = print
}
local code = "print(roleName)"
local func = assert(loadstring(code))
setfenv(func, env)
func() -- 输出:RoleB
和上面 load(..., env) 一样:chunk 里查 roleName、print 都走 env。上一节 setfenv 是绑在已有函数上;这里是绑在 load 出来的函数上。
loadfile 从磁盘读 .lua,也是先拿到函数,再绑环境:
-- 5.2+:第四个参数直接传 env
local func = loadfile("Config.lua", "t", env)
if func then func() end
-- 5.1 / LuaJIT:loadfile 只有路径,用 setfenv 绑环境
local func = assert(loadfile("Config.lua"))
setfenv(func, env)
func()
沙盒限制环境
沙盒 = 给 chunk 一张只含白名单的环境表。
local env = {
print = print,
role = { id = 1001, name = "RoleA" }
}
local code = [[
print(role.id)
print(role.name)
print(os)
]]
local func = load(code, "RoleSandbox", "t", env)
func()
-- 输出:1001
-- 输出:RoleA
-- 输出:nil
env 里没有 os,脚本就看不到 os。
注意不要完整 _G 丢进去:
local env = { print = print, _G = _G }
local func = load("print(_G.os ~= nil)", "BadSandbox", "t", env)
func() -- 输出:true,隔离失效
环境表管的是能查到哪些名字;也可能会改污染外面的表。
local role = { name = "RoleA" }
local env = { role = role }
load("role.name = 'BadRole'", "Modify", "t", env)()
print(role.name) -- 输出:BadRole
禁止声明全局变量
项目里常见忘写 local,解决办法是给 _G 挂 __newindex,在设置元表后新增未声明字段时直接报错:
setmetatable(_G, {
__newindex = function(_, key, value)
error("attempt to write undeclared global: " .. tostring(key), 2)
end
})
框架启动阶段可能还要先注册 Game 等入口,再开拦截。
__newindex 只拦给 _G 新增字段:
_G.Game = {}
setmetatable(_G, {
__newindex = function(_, key, value)
error("attempt to write undeclared global: " .. tostring(key), 2)
end
})
Game.version = "1.0.0" -- 不会触发 _G.__newindex
改的是 Game 内部字段,_G 上没新增 key所以没问题。
总结
全局变量是环境表字段,_ENV 决定变量去哪找,沙盒就是给 chunk 换一张受限制的环境表。
6.2 知识点代码
Lesson6_全局环境与沙盒.lua
print("**********全局环境与沙盒************")
print("**********知识点一 全局变量、环境表和沙盒************")
roleName = "RoleA"
print(roleName) -- 输出:RoleA
print(_G.roleName) -- 输出:RoleA,没 local 时写进环境表
roleName = nil -- 清理演示用全局字段
print("**********知识点二 _G 减负************")
function OpenPanel()
print("Open Bag Panel")
end
function OpenPanel()
print("Open Shop Panel")
end
OpenPanel()
-- 输出:Open Shop Panel,后者盖掉前者
OpenPanel = nil -- 清理演示用全局函数
_G.Game = _G.Game or {}
Game.UI = Game.UI or {}
function Game.UI.OpenPanel(panelName)
print("Open Panel:", panelName)
end
Game.UI.OpenPanel("Bag")
-- 输出:Open Panel: Bag
_G.Game = nil -- 清理演示用全局入口
print("**********知识点三 _ENV 决定变量去哪找************")
if _VERSION ~= "Lua 5.1" then
local env = {
roleName = "RoleA",
print = print
}
local function TestEnv()
local _ENV = env
print(roleName) -- 输出:RoleA,从 env 查 roleName 和 print
end
TestEnv()
local badEnv = { roleName = "RoleA" }
local function TestBadEnv()
local _ENV = badEnv
print(roleName) -- env 里没有 print,会报错
end
local ok, err = pcall(TestBadEnv)
print(ok) -- 输出:false
print(err ~= nil) -- 输出:true
else
print("当前 Lua 5.1 语义,_ENV 不作为环境入口,见知识点四 setfenv")
-- 输出:当前 Lua 5.1 语义,_ENV 不作为环境入口,见知识点四 setfenv
end
print("**********知识点四 Lua 5.1 旧项目用 setfenv************")
if setfenv then
local env51 = {
roleName = "RoleA",
print = print
}
local function TestSetfenv()
print(roleName)
end
setfenv(TestSetfenv, env51)
TestSetfenv() -- 输出:RoleA,setfenv 绑在已有函数上
else
print("当前运行时没有 setfenv,一般是 Lua 5.2+ 语义")
-- 输出:当前运行时没有 setfenv,一般是 Lua 5.2+ 语义
end
print("**********知识点五 chunk 和 load:只看环境替换************")
-- 5.1 / LuaJIT:loadstring + setfenv;5.2+:load(code, chunkName, "t", env)
local function LoadWithEnv(code, chunkName, env)
if setfenv then
local func = assert(loadstring(code))
if env then
setfenv(func, env)
end
return func
end
return assert(load(code, chunkName, "t", env))
end
-- loadfile 同理:5.1 先 loadfile 再 setfenv;5.2+ 用 loadfile(path, "t", env)
-- Lesson 不另建 Config.lua,磁盘 chunk 按上面两套绑 env 即可;字符串演示用 LoadWithEnv
if setfenv then
print("当前 5.1 语义,字符串 chunk 走 loadstring + setfenv")
-- 输出:当前 5.1 语义,字符串 chunk 走 loadstring + setfenv
else
print("当前 5.2+ 语义,字符串 chunk 走 load 第四个参数")
-- 输出:当前 5.2+ 语义,字符串 chunk 走 load 第四个参数
end
_G._chunkRole = "RoleA"
local defaultChunk = LoadWithEnv("print(_chunkRole)")
defaultChunk()
-- 输出:RoleA,默认 chunk 从全局环境读 _chunkRole
_G._chunkRole = nil
local envForChunk = {
_chunkRole = "RoleB",
print = print
}
local envChunk = LoadWithEnv("print(_chunkRole)", "RoleChunk", envForChunk)
envChunk()
-- 输出:RoleB,load 出来的函数绑 env 后从 envForChunk 读 _chunkRole
print("**********知识点六 沙盒就是限制环境表************")
local role = {
id = 1001,
name = "RoleA"
}
local safeEnv = {
print = print,
role = role
}
local safeCode = [[
print(role.id)
print(role.name)
print(os)
]]
local safeFunc = LoadWithEnv(safeCode, "RoleSandbox", safeEnv)
safeFunc()
-- 输出:1001
-- 输出:RoleA
-- 输出:nil,safeEnv 里没有 os
LoadWithEnv("role.name = 'BadRole'", "ModifyRole", safeEnv)()
print(role.name)
-- 输出:BadRole,环境表只管名字查找,表数据仍可能被改
local unsafeEnv = {
print = print,
_G = _G
}
LoadWithEnv("print(_G.os ~= nil)", "UnsafeSandbox", unsafeEnv)()
-- 输出:true,把完整 _G 塞进 env 等于隔离失效
print("**********知识点七 禁止未声明全局************")
local oldMeta = getmetatable(_G)
_G.Game = {} -- 先挂入口,再开 _G.__newindex 拦截
setmetatable(_G, {
__newindex = function(_, key, value)
error("attempt to write undeclared global: " .. tostring(key), 2)
end
})
Game.version = "1.0.0"
print(Game.version) -- 输出:1.0.0,改 Game 字段不会触发 _G.__newindex
local ok, err = pcall(function()
BadGlobal = 1001
end)
print(ok) -- 输出:false
print(err ~= nil) -- 输出:true
_G.Game = nil
setmetatable(_G, oldMeta)
print("**********知识点八 小结************")
print("全局变量是环境表字段,沙盒是换受限制的环境表")
-- 输出:全局变量是环境表字段,沙盒是换受限制的环境表
print("5.1 字符串 chunk 用 loadstring + setfenv;5.2+ 用 load 第四个参数")
-- 输出:5.1 字符串 chunk 用 loadstring + setfenv;5.2+ 用 load 第四个参数
print("loadfile 先拿函数再绑 env;沙盒限制名字查找,不保证数据只读")
-- 输出:loadfile 先拿函数再绑 env;沙盒限制名字查找,不保证数据只读
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com