6.全局环境与沙盒

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 里查 roleNameprint 都走 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

×

喜欢就点赞,疼爱就打赏