- 游戏开发
- 图形学
【免费下载链接】Piccolo
Piccolo (formerly Pilot) – mini game engine for games104
本文以 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。
- 若目标是C 函数,环境总是放在第一个 upvalue(sol2 约定所有 upvalue 名称为空字符串
版本兼容性注意事项
文档明确提醒: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.1 | Lua 5.2+ |
|---|---|---|
| 底层 API | lua_setfenv/lua_getfenv | 搜索/替换_ENVupvalue(C 函数为第一个空名 upvalue) |
| 可挂环境的对象 | 仅 userdata、thread、function | 规则更宽松 |
| 环境存储位置 | 独立的 fenv 槽位 | 函数的_ENVupvalue |
综合文档与源码,使用sol::environment时建议注意:
- 隔离即"全隐藏":无 fallback 的新建环境会隐藏包括库函数在内的一切全局内容,需要手动放行(如
my_env["print"] = lua["print"]),否则脚本会在运行时报访问错误; - 读取前先校验:用
sol::env_key或sol::get_environment提取环境后,先用valid()判断目标是否真的有环境; - 回退只读语义:把
lua.globals()作为 fallback 得到的"可读全局、不可写全局"隔离,适合多模块共享只读配置(如 Piccolo 这类引擎加载 Lua 逻辑时按模块隔离全局命名空间)的场景; - 跨版本移植:若项目需要同时支持 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
相关推荐
sol2 环境封装:用 sol::environment 实现脚本沙箱与环境管理
sol2 环境封装:用 sol::environment 实现脚本沙箱与环境管理 本文基于 sol2 官方 API 文档 environment.rst htt
开发工具ESP-IDF 以太网驱动开发指南:从 IEEE 802.3 帧格式到 EMAC/PHY 配置、TCP/IP 对接与高级功能
ESP IDF 以太网驱动开发指南:从 IEEE 802.3 帧格式到 EMAC/PHY 配置、TCP/IP 对接与高级功能 以太网是 ESP IDF 中最可靠
游戏开发图形学Piccolo 引擎 Lua 脚本实战:sol2 函数注册、重载调用与多返回值完全指南
Piccolo 引擎 Lua 脚本实战:sol2 函数注册、重载调用与多返回值完全指南 本文以 sol2 3.3.0 官方教程 functions.rst ht
游戏开发图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考