7 个版本
0.3.3 | 2024年6月16日 |
---|---|
0.3.2 | 2024年6月16日 |
0.3.1 | 2024年1月1日 |
0.2.0 | 2023年10月30日 |
0.1.0 | 2023年5月5日 |
#24 在 编程语言 中
每月下载量 351
在 8 个 仓库中使用 (直接使用 4 个)
530KB
14K SLoC
piccolo - 一个使用纯 Rust 实现的实验性无状态 Lua 虚拟机
(经过四年,现在不再暂停!)
项目目标,按优先级大致降序排列
- 成为一个可用、有用的 Lua 解释器。
- 提供一个简单的方法来安全地沙盒不受信任的 Lua 脚本。
- 对来自不受信任脚本的 DoS 具有弹性(脚本不应该能够导致解释器崩溃或使用无限量的内存,并且应该保证在有限的时间内将控制权返回给调用者)。
- 提供一个简单的方法将 Rust API 安全地绑定到 Lua,具有能够抵抗古怪性和边缘情况的绑定系统,并且具有可以安全参与运行时垃圾回收的用户类型。
- 在某种程度上与 PUC-Rio Lua 的某些版本兼容。
- 不要慢得令人难以忍受(例如,避免会使解释器比 PUC-Rio Lua 基本上更慢的抽象)。
API 不稳定性
预计 1.0 版本之前的 API 将会频繁更改,这个包仍然非常实验性。所有与 API 不兼容的更改都将伴随着小版本号的增加,但这些将会非常常见。
安全性
piccolo 的目标是用大多数 Rust 代码实现安全。目前,存在一些不安全的来源,但关键的是,这些不安全的来源是 隔离的。piccolo 将不惜一切代价避免依赖泄漏不安全的抽象,即使与 piccolo 的甚至低级细节交互,也不应该需要使用 unsafe。
当前不安全的主要来源
- Lua 表的特殊要求需要使用 hashbrown 的低级 RawTable API。
- Userdata 需要非常精细的不安全生命周期操作,以处理具有安全接口的非 'static userdata 的向下转换。
- 需要不安全代码来避免在几个 Lua 类型中存在胖指针,以使 Value 尽可能小,并允许未来可能的更小的 Value 表示形式。
(piccolo
目前还没有采取措施来防范类似 Spectre 的旁路攻击,因此即使虚拟机内存安全,运行不受信任的脚本也可能存在额外的风险。由于没有 JIT 或回调 API 来准确测量时间,这实际上可能是不可能的。)
Rust 与 GC 交互的独特系统
piccolo
的垃圾回收器系统现在已经在其 自己的仓库 中,也已在 crates.io 上发布。有关 GC 设计的更多详细信息,请参阅链接仓库中的 README。
piccolo
拥有真正的、循环检测的、增量垃圾回收器,带有零成本的 Gc
指针(它们的大小与机器指针相同,并实现了 Copy
)。这些指针可以从安全的 Rust 中使用。它是通过结合以下两个方面实现的
- 一个不安全的
Collect
特性,它允许通过垃圾回收的类型进行跟踪,尽管这些类型是不安全的,但可以使用过程宏安全地实现。 - 通过唯一的、不变的“生成”生存期对
Gc
指针进行标记,以确保这些指针仅隔离于单个根对象,并保证在mutate
激活的调用之外,所有这些指针要么可从根对象访问,要么可以安全地回收。
无栈虚拟机
基于 mutate
的 GC API 意味着长时间运行的 mutate
调用可能会出现问题。在调用 mutate
期间无法进行垃圾回收,因此我们必须确保定期从 mutate
调用中返回,以便进行垃圾回收。
因此,piccolo
中的虚拟机是按所谓的“无栈”或“跳板”风格编写的。它不依赖于 Rust 栈来进行 Lua -> Rust 和 Rust -> Lua 嵌套,相反,回调可以具有某种即时结果(返回值、从协程中产生值、恢复线程、错误),或者它们可以产生一个 Sequence
。一个 Sequence
类似于 Future
,它是一个多步骤操作,父 Executor
将驱动它完成。 Executor
将重复调用 Sequence::poll
直到序列完成,而 Sequence
在被轮询的同时可以产生值并调用任意的 Lua 函数。
例如,Lua 可以调用 Rust 回调,然后 Rust 回调创建一个新的 Lua 协程并运行它。为了这样做,回调需要将 Lua 函数作为参数,然后从它创建一个新的 Thread
并返回 SequencePoll:Resume
以运行它。外部主 Executor
将运行创建的 Thread
,当它完成时,将通过 Sequence::poll
(或 Sequence::error
)"返回"。这正是 Lua stdlib 函数 coroutine.resume
的实现方式。
作为另一个例子,pcall
在这里很容易实现,回调可以调用提供的函数,并在其下方传递一个 Sequence
,序列可以捕获错误并返回错误状态。
另一个例子,想象 Rust 代码调用 Lua 协程线程,该线程调用一个 Rust Sequence
,该 Sequence
调用更多的 Lua 代码,然后产生值。我们的堆栈将看起来像这样
[Rust] -> [Lua Coroutine] -> [Rust Sequence] -> [Lua code that yields]
这种虚拟机风格的实现没有问题,内部Rust回调被暂停作为一个Sequence
,内部yield会返回值直到顶层Rust代码。当协程线程恢复并最终返回时,Rust的Sequence
也会恢复。
无论嵌套多少个Lua线程和Sequence
,控制权总会连续返回到外部Rust代码,这是这里的“trampoline”。当使用这个解释器时,某个地方有一个循环,它不断地调用Arena::mutate
和Executor::step
,并且可以在任何时候停止、暂停或更改任务,无需展开Rust堆栈。
这种“无堆栈”风格有许多好处,它允许实现一些其他虚拟机中难以实现的并发模式(如tasklets),并使虚拟机对不受信任的脚本DoS攻击具有更强的抵抗力。
“无堆栈”风格的缺点是,有时将内容编写为Sequence
比编写正常的、直接的流程控制要困难得多。如果异步Rust/生成器能够在这里有所帮助,那就太好了,这将允许轻松实现Sequence
,但是目前存在多个编译器限制,使得这一实现变得不可行,或者变得如此不人性化,以至于不再值得。
执行器“燃料”和虚拟机内存跟踪
无堆栈虚拟机风格“定期”将控制权返回到驱动一切的外部Rust代码,这种情况发生的频率可以使用“燃料”系统来控制。
Lua和Lua驱动的回调代码总是在某个Executor::step
调用中发生。这个方法接受一个fuel
参数,它控制虚拟机在暂停之前应该运行多长时间,燃料大致以虚拟指令单元衡量。
提供给Executor::step
的不同数量的燃料限制了Lua执行的数量,限制了CPU时间和单个Executor::step
调用中可以发生的内存分配量(假设遵循某些关于提供的回调的规则)。
虚拟机现在还使用gc-arena
内存跟踪功能,准确地跟踪其内部gc-arena::Arena
中分配的所有内存。这可以扩展到userdata和userdata API,并且在暴露的userdata和回调中遵循正确的规则,允许进行准确的内存报告和内存限制。
假设这两种机制都工作正常,并且假设所有回调/userdata API都遵循相同的规则,这不仅允许在内存安全和API访问方面沙盒化不受信任的脚本,还允许在CPU和RAM使用方面进行沙盒化。然而,这些都是很大的假设,而且piccolo
仍然是非常多的WIP,因此确保正确执行是一项持续的工作。
目前的工作情况
- 实际周期检测、增量GC,类似于PUC-Rio Lua 5.3/5.4中的增量收集器
- Lua源代码被编译成类似于PUC-Rio Lua的虚拟机字节码,并实现了完整的虚拟机指令集
- 几乎所有的核心Lua语言都工作正常。一些目前实际上工作的复杂Lua功能
- 真实的闭包,具有适当的upvalue处理
- 正确的尾调用
- 可变参数和返回,以及通常正确的vararg(
...
)处理 - 协程,包括对Rust回调透明的yield
- 带有标签处理的goto,与Lua 5.3/5.4匹配
- 正确处理_ENV
- 元表和元方法,包括能够触发其他元方法的完全递归元方法(并非所有元方法都已实现,特别是
__gc
终结器)。
- 一个健壮的Rust回调系统,具有不阻塞解释器的序列回调,并允许在不使用Rust栈的情况下调用和返回Lua。
- 具有安全向下转换的垃圾回收“用户数据”。
- stdlib中的一些功能尚未实现(stdlib的大部分核心、基本部分已实现,例如
coroutine
库、pcall
、error
,几乎所有公开基本运行时特性的功能都已实现)。 - 一个简单的REPL(尝试使用
cargo run --example interpreter
)
目前不工作的事情
- stdlib的大部分功能尚未实现。大多数“外围”部分都是这样,例如
io
、file
、os
、package
、string
、table
和utf8
库要么缺失,要么实现得非常稀疏。 - 目前还没有支持终结。
gc-arena
现在以支持终结的方式支持终结,应该可以完全实现具有复活和弱键/值和瞬态表的__gc
元方法,但这尚未完成。目前,__gc
元方法没有效果。 - 编译的VM代码在某些方面比PUC-Rio Lua生成的代码要差。值得注意的是,有一个尚未实现的JMP链优化,这使得大多数循环的速度比PUC-Rio Lua慢得多。
- 不会让你哭泣的错误信息
- 堆栈跟踪
- 调试器
- 积极的优化和确保它在所有情况下都与PUC-Rio Lua的性能相匹配的真正努力。
- 我可能还忘记了很多
可能永远都不会实现的事情
这不是一个详尽的列表,但这些是目前我认为几乎肯定不是目标的事情。
- 与PUC-Rio Lua C API兼容的API。实现起来非常困难,会非常慢,其中一些基本上是不可能的(longjmp错误处理和相邻行为)。
- 与PUC-Rio Lua中某些类行为的完美兼容性
- PUC-Rio Lua的行为会因操作系统、环境、编译设置、系统区域设置等因素而异(在PUC-Rio Lua的某些版本中,甚至词法分析器的行为也取决于系统区域设置!)
piccolo
更多或更少地试图通过将luaconf.h
中默认设置的“C”区域设置为64位Linux上的目标来模拟PUC-Rio Lua的行为。 - 错误信息的特定格式。
- 表的特定迭代顺序,以及长度运算符的特定行为(长度运算符目前正确地工作,并且总是会返回一个表“边界”,但对于非序列表,返回的边界选择可能会有所不同)。
- PUC-Rio Lua的行为会因操作系统、环境、编译设置、系统区域设置等因素而异(在PUC-Rio Lua的某些版本中,甚至词法分析器的行为也取决于系统区域设置!)
debug
库尚未实现,其中大部分可能永远不会实现,因为存在根本的VM差异。- 与PUC-Rio Lua字节的兼容性
os.setlocale
和从C继承的其他怪异之处package.loadlib
和允许加载C库的所有功能。- 与PUC-Rio Lua中的(有时相当奇特)垃圾收集器角落案例行为完美匹配。
为什么叫‘piccolo’?
这是一个可爱的小“pico”Lua,明白了吗?
它其实并不完全算得上是“微型”,但仍然是一个可爱的小型仪器,您可以安全地带到任何地方!
这个项目不是叫别的名字吗?光辉?迪莫斯?
有一次令人尴尬的命名混乱,我不知为何两次都使用了别人的项目名称。它们都是同一个项目。我保证我已经完成了重命名。
许可证
piccolo
的许可证是以下其中之一
- MIT许可证 LICENSE-MIT 或 http://opensource.org/licenses/MIT
- 创意共享CC0 1.0通用公共领域贡献 LICENSE-CC0 或 https://creativecommons.org/publicdomain/zero/1.0/
任选其一。
依赖项
~2.2–3MB
~50K SLoC