加载Lua函数块,在5.2版本中使用沙盒修改环境

3
这个问题类似于修改Lua块环境,但有所不同。
我想将字符串加载为可重用的Lua函数,在评估时它们都共享相同的沙盒,但每次调用时我可以传递不同的变量值。
例如,假设我想评估方程round(10*w/h) + a * b,其中roundwh都是一个共同的沙盒环境中的值,但ab是我想要每次评估方程时修改的值。 这个问题展示了如何动态更改编译函数的环境。然而,它设置了函数的整个环境,没有我的回退沙盒。
有什么有效的方法来实现我的目标?请注意,我几乎只关心评估函数所需的时间,而不是设置时间。
我现在的做法是,用户编写类似以下的 CSS 表达式:
box {
   left: @x;
   width: viewwidth - @w;
}

“...其中@x@w是盒子元素的属性('local' 变量),而viewwidth是在其他地方设置的表级变量(我的沙盒)。每个属性值——冒号后面的部分——都被解析为一个字符串,以编译成 Lua 函数。它们使用普通的 Lua 语法,除了我目前使用 _el. 来交换 @,以便对元素表进行取消引用。

对于这个问题的答案,可以保持相同的语法,并要求区分本地变量和表变量,但也可以有一个解决方案,去掉@符号并处理所有变量相同。”


谁决定哪些变量是可修改的,哪些来自环境?是你要评估的字符串还是外部世界? - Nicol Bolas
@NicolBolas 字符串是一个CSS表达式。'sandbox'变量由用户在一个地方设置,而'local'变量则是匹配元素的属性。 - Phrogz
“用户在一个地方设置了‘沙盒’变量,‘本地’变量是匹配元素的属性。” 但是,你的回答中包含了一种解决方案,要求用户明确注释作为接口的一部分的变量。这些注释会阻止它成为“CSS表达式”。那么,关于适当答案的要求究竟是什么?请将其编辑到问题中。” - Nicol Bolas
@NicolBolas 我已经完成了。 - Phrogz
1个回答

1
我想出了6种实现目标的技巧。本文底部列出了它们的性能基准,以评估速度。最快的技术需要在代码中区分局部变量和沙盒变量。
local SANDBOX = {
  w=1920,
  h=1080,
  round=function(n) return math.floor(n+0.5) end
}
local CODE = "round(10*w/h) + @a * @b"

function compile(code, sandbox)
  local munged = 'local _el=... return '..code:gsub('@', '_el.')
  return load(munged, nil, nil, sandbox)
end

local f = compile(CODE, SANDBOX)
print(f{a=1, b=2}) --> 20
print(f{a=3, b=4}) --> 30

如果您不想区分更改的变量和沙盒中的变量,或者不想使用像上面那样脆弱的sub()函数,下一个最快的方法是改变本地变量的__index,然后将其作为环境传递。您可以将此包装在助手函数中,以使其更容易:
local CODE = "round(10*w/h) + a * b"

function compile(code, sandbox)
  local meta = {__index=sandbox}
  return {meta=meta, f=load('_ENV=... return '..code)}
end

function eval(block, locals)
  return block.f(setmetatable(locals, block.meta))
end

local f = compile(CODE, SANDBOX)
print(eval(f, {a=1, b=2})) --> 20
print(eval(f, {a=3, b=4})) --> 30

以下是所有技术的基准测试结果。请注意,最快的技术可能会更快,因为与所有其他技术不同,编译函数返回一个可以直接调用的函数,而不是包装在评估辅助脚本中:
scope as a table, 2              : 0.9s (0.22µs per eval)
scope as a table                 : 1.1s (0.27µs per eval)
Use __ENV, change scope meta     : 1.3s (0.32µs per eval)
blank env, change meta of scope  : 1.6s (0.41µs per eval)
copy values over environment     : 2.8s (0.70µs per eval)
setfenv, change scope meta       : 3.0s (0.74µs per eval)

local SANDBOX = {
    w     = 1920,
    h     = 1080,
    round = function(n) return math.floor(n+0.5) end
}
local TESTS = {
    {env={a=1, b=2}, expected=18+2},
    {env={a=4, b=3}, expected=18+12},
    {env={a=9, b=7}, expected=18+63},
    {env={a=4, b=5}, expected=18+20},
}

-- https://leafo.net/guides/setfenv-in-lua52-and-above.html
local function setfenv(fn, env)
    local i = 1
    while true do
        local name = debug.getupvalue(fn, i)
        if name == "_ENV" then
            debug.upvaluejoin(fn, i, (function() return env end), 1)
            break
        elseif not name then
            break
        end
        i = i + 1
    end
    return fn
end

local techniques = {
    ["copy values over environment"]={
        code="round(10*w/h) + a*b",
        setup=function(code, fallback)
            local env = setmetatable({},{__index=fallback})
            return {env=env,func=load("return "..code,nil,nil,env)}
        end,
        call=function(block, kvs)
            for k,v in pairs(block.env) do block.env[k]=nil end
            for k,v in pairs(kvs) do block.env[k]=v end
            return block.func()
        end
    },
    ["blank env, change meta of scope"]={
        code="round(10*w/h) + a*b",
        setup=function(code, fallback)
            local kvsmeta = {__index=fallback}
            local envmeta = {}
            local env = setmetatable({},envmeta)
            return {envmeta=envmeta,meta=meta,kvsmeta=kvsmeta,func=load("return "..code,nil,nil,env)}
        end,
        call=function(block, kvs)
            block.envmeta.__index=kvs
            setmetatable(kvs, block.kvsmeta)
            return block.func()
        end
    },
    ["setfenv, change scope meta"]={
        code="round(10*w/h) + a*b",
        setup=function(code, fallback)
            return {meta={__index=fallback}, func=load("return "..code)}
        end,
        call=function(block, kvs)
            setmetatable(kvs,block.meta)
            setfenv(block.func, kvs)
            return block.func()
        end
    },
    ["Use __ENV, change scope meta"]={
        code="round(10*w/h) + a*b",
        setup=function(code, fallback)
            local meta = {__index=fallback}
            return {meta=meta, func=load("_ENV=... return "..code)}
        end,
        call=function(block, kvs)
            return block.func(setmetatable(kvs, block.meta))
        end
    },
    ["scope as a table"]={
        -- NOTE: requires different code than all other techniques!
        code="round(10*w/h) + _el.a * _el.b",
        setup=function(code, fallback)
            local env = setmetatable({},{__index=fallback})
            return {env=env,func=load("return "..code,nil,nil,env)}
        end,
        call=function(block, kvs)
            block.env._el=kvs
            return block.func()
        end
    },
    ["scope as a table, 2"]={
        -- NOTE: requires different code than all other techniques!
        code="round(10*w/h) + _el.a * _el.b",
        setup=function(code, fallback)
            return load("local _el=... return "..code,nil,nil,fallback)
        end,
        call=function(func, kvs)
            return func(kvs)
        end
    },
}

function validate()
    for name,technique in pairs(techniques) do
        local f = technique.setup(technique.code, SANDBOX)
        for i,test in ipairs(TESTS) do
            local actual = technique.call(f, test.env)
            if actual~=test.expected then
                local err = ("%s test #%d expected %d but got %s\n"):format(name, i, test.expected, tostring(actual))
                io.stderr:write(err)
                error(-1)
            end
        end
    end
end

local N = 1e6
function benchmark(setup, call)
    for name,technique in pairs(techniques) do
        local f = technique.setup(technique.code, SANDBOX)
        local start = os.clock()
        for i=1,N do
            for i,test in ipairs(TESTS) do
                technique.call(f, test.env)
            end
        end
        local elapsed = os.clock() - start
        print(("%-33s: %.1fs (%.2fµs per eval)"):format(name, elapsed, 1e6*elapsed/#TESTS/N))
    end
end

validate()
benchmark()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接