☰
sol2 的 sol::environment 完全指南:用 Lua 表实现脚本沙箱与运行环境隔离(Piccolo 引擎)
2026/10/15 2:02:39 网站建设 项目流程
  • 游戏开发
  • 图形学

【免费下载链接】Piccolo

Piccolo (formerly Pilot) – mini game engine for games104

项目地址:https://gitcode.com/gh_mirrors/pi/Piccolo
点击查看免费下载

本文以 sol2-3.3.0 官方 API 文档 environment.rst 为核心,结合 Piccolo 引擎第三方库目录下的 sol2 源码、示例与测试,系统讲解sol::environment的构造、set_on/set_environment/get_environment的底层实现、在sol::state脚本执行中的沙箱用法,以及 Lua 5.1 与 5.2+ 两代运行时的环境机制差异。读完本文,你将掌握如何为同一 Lua 状态创建互不污染、可回退到全局表的隔离运行环境,并能在 C++ 侧安全地检索与注入环境。

一、概述:sol::environment是什么

sol::environment是 sol2 提供的脚本沙箱封装类型,官方文档将其定位为 "encapsulation table for script sandboxing"。它的核心类型关系如下:

class environment : public table;

即environment直接继承自sol::table,而sol::table又继承自sol::reference(参考 environment.hpp 中basic_environment : basic_table<base_type>的定义)。因此:

  • environment本质上就是一张普通 Lua 表,sol::table支持的所有表操作(读写字段、遍历、调用元方法等,见 table 文档)对它全部适用;
  • 由于继承链的引用语义,把一个environment拷贝给另一个environment不会深拷贝表内容,只是创建了一个指向同一 Lua 对象的新引用。

沙箱的核心机制

该类型会被传递给sol::state(_view)的script/do_x系列函数(详见 state 文档),用于提供脚本运行时的局部变量读写环境。借助表的元表机制,你可以做出非常灵活的隔离策略,例如:

  • 用 sol::metatable_key 为环境表设置元表;
  • 让元表的__index指向全局表_G,从而实现只读地查询全局变量、但不能直接写入全局变量——脚本写入var = x时只会落在当前环境表中,而读取时若环境表中不存在,则回退到全局表查找。

适用对象

sol::environment可用于:

  • 脚本调用(script calls):作为lua.script(code, env)的第二个参数;
  • 函数(set on functions):通过sol::set_environment(env, fn)或env.set_on(fn)挂接到 Lua 函数上;
  • 线程(threads):同样可应用于 sol::thread(协程)。

二、构造环境:sol::create与四种构造函数

sol::environment的构造函数(来自文档 members 一节):

environment(lua_State* L, sol::new_table nt); environment(lua_State* L, sol::new_table nt, const sol::reference& fallback); environment(sol::env_key_t, const sol::reference& object_that_has_environment); environment(sol::env_key_t, const sol::stack_reference& object_that_has_environment);

前两个沿用sol::table的构造方式,特别常用的是:

sol::environment my_env(lua, sol::create);

这会创建一张全新的、未命名的环境表。其中sol::create是sol::new_table类型的便捷常量,其内部带有sequence_hint/map_hint两个容量提示参数,帮助 Lua 预分配合适大小的表结构(99% 的场景直接用sol::create即可,详见 new_table 文档)。

带 fallback 的构造:回退表

sol::environment my_env(lua, sol::create, my_fallback_table);

第三个参数fallback作为环境表的查找回退:当环境表中找不到某个键时,转而去 fallback 中查询。其实现位于 environment.hpp:

template <bool b> basic_environment(lua_State* L, new_table t, const basic_reference<b>& fallback) : basic_environment(L, std::move(t)) { stack_table mt(L, new_table(0, 1)); mt.set(meta_function::index, fallback); this->set(metatable_key, mt); mt.pop(); }

也就是说,sol2 只是简单地新建一张元表、把__index指向 fallback、再把这张元表挂到环境表上。理解了这一点,你就可以通过 sol::metatable_key 自定义元表实现更复杂的效果(比如组合多个回退、接入代理逻辑等)。

sol::env_key构造:从已有对象提取环境

后两个构造函数接收一个特殊空类型sol::env_key_t(其唯一的用途就是触发该构造函数),配合便捷常量sol::env_key使用:

sol::environment my_env(sol::env_key, some_object);

它会从第二个参数(可能是sol::object、sol::function、sol::table、sol::userdata等任何引用类型)中提取其所属的环境。若目标对象没有环境,构造函数仍会完成,但该对象会引用 Lua 的nil,即env.valid() == false。

从源码看,env_key_t构造和get_environment最终都依赖stack::push_environment_of(见 stack_push.hpp):在 Lua 5.1 下直接调用lua_getfenv;在 5.2+ 下则遍历 upvalue 搜索名为"_ENV"的变量(C 函数则取第一个空名字 upvalue),找不到时压入lua_nil。这正是"没有环境则 valid() 为 false"的来源。

三、应用环境:set_on与sol::set_environment

设置环境有两种等价写法:

sol::set_environment(my_env, some_reference); // 等价于 my_env.set_on(some_reference);

文档给出的签名:

template <typename T> void set_on(const T& target);

set_on的底层实现(environment.hpp)充分体现了 Lua 两个版本的差异:

  • Lua 5.1 及更早:调用lua_setfenv(L, target_index)直接为目标设置函数环境,返回非 0 即成功;
  • Lua 5.2+:不再有setfenv全局函数,环境是函数的一个 upvalue。sol2 的实现分为两种情况:
    • 若目标是C 函数,环境总是放在第一个 upvalue(sol2 约定所有 upvalue 名称为空字符串""),通过lua_setupvalue(L, target_index, 1)写入;
    • 若目标是Lua 函数,则从第 1 个 upvalue 开始逐个调用lua_getupvalue搜索名为"_ENV"的 upvalue,找到后替换之;找不到则返回false。

版本兼容性注意事项

文档明确提醒:Lua 5.1 只允许将环境应用到 userdata、thread 和 function 上;5.2+ 的规则则更为宽松(几乎所有可挂 upvalue 的对象都可行)。编写跨版本代码时应留意这一差异。

四、读取环境:sol::get_environment

template <typename E = reference, typename T> basic_environment<E> get_environment(const T& target);

自由函数get_environment从目标对象中取回环境,返回basic_environment<E>(默认为E = reference,即sol::environment)。其实现(environment.hpp)就是一次stack::push_environment_of加一次引用包装:

template <typename E = reference, typename T> basic_environment<E> get_environment(const T& target) { lua_State* L = target.lua_state(); auto pp = stack::pop_n(L, stack::push_environment_of(target)); return basic_environment<E>(L, -1); }

文档指出:每个普通 Lua 函数、可执行脚本等都有环境;在某些版本的 Lua 运行时中 userdata 也有环境。如果目标没有有效环境,创建出的 environment 的valid()会返回false,因此使用前建议用env.valid()做检查。

五、实战示例:从官方 example 到仓库测试

1. 基础沙箱:preparing_environments.cpp

文档正文引用的完整示例位于 preparing_environments.cpp:

#define SOL_ALL_SAFETIES_ON 1 #include <sol/sol.hpp> int main(int, char*[]) { sol::state lua; lua.open_libraries(); sol::environment my_env(lua, sol::create); // set value, and we need to explicitly allow for // access to "print", since a new environment hides // everything that's not defined inside of it // NOTE: hiding also hides library functions (!!) // BE WARNED my_env["var"] = 50; my_env["print"] = lua["print"]; sol::environment my_other_env( lua, sol::create, lua.globals()); // do not need to explicitly allow access to "print", // since we used the "Set a fallback" version // of the sol::environment constructor my_other_env["var"] = 443; // output: 50 lua.script("print(var)", my_env); // output: 443 lua.script("print(var)", my_other_env); return 0; }

运行输出:

50 443

这个例子揭示了沙箱的两个关键行为:

  • 全新环境是"隔离但不继承"的:my_env中没定义的东西(包括print等所有库函数)都不可见,所以必须手动my_env["print"] = lua["print"]放行;
  • 带 fallback 的环境自动继承全局表:my_other_env把lua.globals()作为 fallback,print等全局资源天然可用,无需逐个授权。

2. 全局回退的验证:environments.cpp

示例 environments.cpp 演示了"能读全局、不能写全局"的效果:先在全局设置lua["b"] = 2142,随后:

sol::environment plain_env(lua, sol::create); lua.script("a = 24", plain_env); // 只写入 plain_env sol::environment env_with_fallback(lua, sol::create, lua.globals()); lua.script("a = 56", env_with_fallback); // 只写入 env_with_fallback

对两个环境分别检查变量a与b后可以看到:脚本在环境内赋值的a只存在于各自环境表中,而全局变量b对带 fallback 的环境可读、对无 fallback 的环境不可见。

3. 函数级环境:environments_on_functions.cpp

示例 environments_on_functions.cpp 展示了把环境挂到函数上的完整流程:

lua.script("f = function() return test end"); sol::function f = lua["f"]; sol::environment env_f(lua, sol::create); env_f["test"] = 31; bool ok = sol::set_environment(env_f, f); // f() 现在返回环境表中的值 31 lua.script("g = function() test = 5 end"); sol::function g = lua["g"]; sol::environment env_g(lua, sol::create); env_g.set_on(g); // 等价写法 g(); // 写入的是 env_g["test"] // 全局表未被污染:lua["test"] 是无效对象

它同时演示了从 C++ 侧反查环境:

// 方式一:env_key 构造 sol::environment target_env(sol::env_key, target); // 方式二:自由函数 sol::environment target_env = sol::get_environment(target);

两种方式都能取回与env_f/env_g相同的环境(示例中用sol_c_assert(env_f == target_env)断言验证)。

4. 仓库测试佐证:this_environment.cpp

在 tests/environment/source/this_environment.cpp 中,environments/this_environment测试用例验证了环境隔离与回退的数值语义:lua["x"] = 5、e["x"] = 20时:

  • 以环境e执行safe_script(code, e),f(10)返回10 + 20 = 30(环境优先);
  • 从 C++ 侧e.set_on(f)后再调用,同样返回30;
  • 不设环境直接调用f(10),则回退到全局表,返回10 + 5 = 15。

六、在 state 中执行脚本时使用环境

sol::state/sol::state_view为带环境的脚本执行提供了专门的重载(见 state_view.hpp):

protected_function_result safe_script(const string_view& code, const basic_environment<E>& env, const std::string& chunkname = detail::default_chunk_name(), load_mode mode = load_mode::any);

而script系列(script/script_file、safe_script/unsafe_script)均以环境为可选参数转发到上述实现,文档(state.rst)给出的签名摘要:

protected_function_result script(const string_view& code, const environment& env, const std::string& chunk_name = "[string]", load_mode mode = load_mode::any); protected_function_result script_file(const std::string& filename, const environment& env, load_mode mode = load_mode::any);

更底层的do_string(code, env)/do_file(filename, env)则只负责加载并执行、不做回调包装。这种设计非常适合在同一个 Lua 状态内运行多个相互隔离的"视图"或"视角":不同模块各自持有独立的sol::environment,通过 fallback 共享只读的全局资源,同时保证写入互不干扰。若需要更强的隔离(连全局都不共享),文档建议考虑创建独立 state 或其他沙箱方案。

在 C++ 函数中感知调用环境:sol::this_environment

与sol::this_state类似,sol::this_environment是一个透明参数:C++ 绑定函数可以把它声明在参数列表中,调用方无需在 Lua 侧传参(详见 this_environment 文档):

void env_check(sol::this_state ts, int x, sol::this_environment te) { if (!te) { // 函数没有环境 return; } sol::environment& env = te; // 取出调用方的环境 // ... }

其实现(environment.hpp)通过 Lua Debug API 定位当前调用帧、再用env_key构造取出环境;取不到时this_environment的optional<environment>为空,可通过if (te)判断。这为"根据脚本来源环境决定 C++ 行为"提供了便捷入口。

七、版本差异、安全边界与使用建议

关注点Lua 5.1Lua 5.2+
底层 APIlua_setfenv/lua_getfenv搜索/替换_ENVupvalue(C 函数为第一个空名 upvalue)
可挂环境的对象仅 userdata、thread、function规则更宽松
环境存储位置独立的 fenv 槽位函数的_ENVupvalue

综合文档与源码,使用sol::environment时建议注意:

  1. 隔离即"全隐藏":无 fallback 的新建环境会隐藏包括库函数在内的一切全局内容,需要手动放行(如my_env["print"] = lua["print"]),否则脚本会在运行时报访问错误;
  2. 读取前先校验:用sol::env_key或sol::get_environment提取环境后,先用valid()判断目标是否真的有环境;
  3. 回退只读语义:把lua.globals()作为 fallback 得到的"可读全局、不可写全局"隔离,适合多模块共享只读配置(如 Piccolo 这类引擎加载 Lua 逻辑时按模块隔离全局命名空间)的场景;
  4. 跨版本移植:若项目需要同时支持 Lua 5.1 与 5.2+,应避免依赖各版本特有的 fenv 行为,统一使用sol::set_environment/env.set_on等高层 API,让 sol2 帮你处理版本差异(如上表所示的分支逻辑位于 environment.hpp 与 stack_push.hpp)。

小结

sol::environment的本质是一张可挂元表、可被引用复制的普通 Lua 表,sol2 通过它把 Lua 的运行环境抽象成了 C++ 侧的一等公民:既能作为脚本执行的沙箱边界,也能挂到函数、userdata 与线程上,还能用sol::env_key/sol::get_environment反查回来。配合 fallback 构造与sol::this_environment透明参数,你可以在同一 Lua 状态下轻松搭建"隔离写入、受控读取、按模块分环境"的脚本架构——这正是 sol2 文档中 "encapsulation table for script sandboxing" 一句的完整含义。

  • 游戏开发
  • 图形学

【免费下载链接】Piccolo

Piccolo (formerly Pilot) – mini game engine for games104

项目地址:https://gitcode.com/gh_mirrors/pi/Piccolo
点击查看免费下载
上一篇:5步打造高效Windows工作流:Komorebi窗口管理器实战指南
下一篇:最硬核的前端工程化工具:Rust驱动的Volta架构深度剖析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询