#lua #vm #stackless #garbage-collection #implemented #experimental #lua-script

piccolo

使用纯 Rust 实现的无状态 Lua 虚拟机

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编程语言

Download history 23/week @ 2024-05-04 75/week @ 2024-05-11 127/week @ 2024-05-18 42/week @ 2024-05-25 73/week @ 2024-06-01 90/week @ 2024-06-08 418/week @ 2024-06-15 34/week @ 2024-06-22 41/week @ 2024-06-29 96/week @ 2024-07-06 109/week @ 2024-07-13 40/week @ 2024-07-20 49/week @ 2024-07-27 75/week @ 2024-08-03 84/week @ 2024-08-10 129/week @ 2024-08-17

每月下载量 351
8 仓库中使用 (直接使用 4 个)

MIT 许可证

530KB
14K SLoC

crates.io docs.rs Build Status Chat

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 中使用。它是通过结合以下两个方面实现的

  1. 一个不安全的 Collect 特性,它允许通过垃圾回收的类型进行跟踪,尽管这些类型是不安全的,但可以使用过程宏安全地实现。
  2. 通过唯一的、不变的“生成”生存期对 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::mutateExecutor::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 库、pcallerror,几乎所有公开基本运行时特性的功能都已实现)。
  • 一个简单的REPL(尝试使用 cargo run --example interpreter

目前不工作的事情

  • stdlib的大部分功能尚未实现。大多数“外围”部分都是这样,例如 iofileospackagestringtableutf8 库要么缺失,要么实现得非常稀疏。
  • 目前还没有支持终结。 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的行为。
    • 错误信息的特定格式。
    • 表的特定迭代顺序,以及长度运算符的特定行为(长度运算符目前正确地工作,并且总是会返回一个表“边界”,但对于非序列表,返回的边界选择可能会有所不同)。
  • debug 库尚未实现,其中大部分可能永远不会实现,因为存在根本的VM差异。
  • 与PUC-Rio Lua字节的兼容性
  • os.setlocale 和从C继承的其他怪异之处
  • package.loadlib 和允许加载C库的所有功能。
  • 与PUC-Rio Lua中的(有时相当奇特)垃圾收集器角落案例行为完美匹配。

为什么叫‘piccolo’?

这是一个可爱的小“pico”Lua,明白了吗?

它其实并不完全算得上是“微型”,但仍然是一个可爱的小型仪器,您可以安全地带到任何地方!

这个项目不是叫别的名字吗?光辉?迪莫斯?

有一次令人尴尬的命名混乱,我不知为何两次都使用了别人的项目名称。它们都是同一个项目。我保证我已经完成了重命名。

许可证

piccolo的许可证是以下其中之一

任选其一。

依赖项

~2.2–3MB
~50K SLoC