crates.io docs.rs Build Status

piccolo - An experimental Lua VM implemented in pure Rust

(After four years, now UN-paused!)

Project Goals: * Be an arguably working, useful Lua interpreter. * Be an easy way to confidently sandbox untrusted Lua scripts. * Be somewhat resilient against DoS from untrusted scripts (scripts should not be able to cause the interpreter to panic and should be guaranteed to pause in some reasonable bounded amount of time). * Be an easy way to bind Rust APIs to Lua safely, with a bindings system that is resilient against weirdness and edge cases, and with user types that can safely participate in runtime garbage collection. * Be pragmatically compatible with some version(s) of PUC-Rio Lua. * Don't be obnoxiously slow (for example, avoid abstractions that would make the interpreter fundamentally slower than PUC-Rio Lua).

This project is currently very WIP Right now, the short term goal is to get some usable subset of Lua working, and to have a robust bindings story. piccolo is being worked on again to use in a separate game project, and my immediate goals are going to be whatever that project requires.

API Instability

Expect frequent pre-1.0 API breakage, this crate is still very experimental. All API incompatible changes will be accompanied by minor version bumps, but these will be very common.

Safety

The goal with piccolo is to have the majority of it written in safe Rust. Currently, there are a few sources of unsafety, but crucially these sources of unsafety are isolated. piccolo will avoid at all costs relying on abstractions which leak unsafety, it should always be possible to interact with even low level details of piccolo without using unsafe.

The current primary sources of unsafety: * The particularly weird requirements of Lua tables require using hashbrown's low level RawTable API. * Userdata requires a very delicate unsafe lifetime dance to deal with downcasting non-'static userdata with a safe interface. * Unsafe code is required to avoid fat pointers in several Lua types, to keep Value as small as possible and allow potential future smaller Value representations.

(piccolo makes no attempt yet to guard against side channel attacks like spectre, so even *if the VM is memory safe, running untrusted scripts carries additional risk)*.

A unique system for Rust <-> GC interaction

The garbage collector system for piccolo is now in its own repo, and also on crates.io. See the README in the linked repo for more detail about the GC design.

piccolo has a real, cycle detecting, incremental garbage collector with zero-cost Gc pointers (they are machine pointer sized and implement Copy) that are usable from safe Rust. It achieves this by combining two things:

1) An unsafe Collect trait which allows tracing through garbage collected types that, despite being unsafe, can be implemented safely using procedural macros. 2) Branding Gc pointers by unique, invariant "generative" lifetimes to ensure that such pointers are isolated to a single root object, and to guarantee that, outside an active call to mutate, all such pointers are either reachable from the root object or are safe to collect.

Stackless VM

The mutate based GC api means that long running calls to mutate can be problematic. No garbage collection can take place during a call to mutate, so we have to make sure to regularly return from the mutate call to allow garbage collection to take place.

The VM in piccolo is thus written in what is sometimes called "stackless" or "trampoline" style. It does not rely on the rust stack for Lua -> Rust and Rust -> Lua nesting, instead callbacks can either have some kind of immediate result (return values, yield values from a coroutine, error), or they can produce a Sequence. A Sequence is a bit like a Future in that it is a multi- step operation that the parent Thread will drive to completion. Thread will repeatedly call Sequence::poll until the sequence is complete, and the Sequence can yield values and call arbitrary Lua functions while it is being polled.

As an example, it is of course possible for Lua to call a Rust callback, which then in turn creates a new Lua coroutine and runs it. In order to do so, a callback would take a Lua function as a parameter, then create a new coroutine Lua thread and return a Sequence impl that will run it. The outer main Lua thread will step the created Sequence, which will in turn step the inner Lua thread. This is exactly how the coroutine.resume Lua stdlib function is implemented.

As another example, pcall is easy to implement here, a callback can call the provided function with a Sequence underneath it, and the sequence can catch the error and return the error status.

Yet another example, imagine Rust code calling a Lua coroutine thread which calls a Rust Sequence which calls yet more Lua code which then yields. Our stack will look something like this:

[Rust] -> [Lua Coroutine] -> [Rust Sequence] -> [Lua code that yields]

This is no problem with this VM style, the inner Rust callback is paused as a Sequence, and the inner yield will return the value all the way to the top level Rust code. When the coroutine thread is resumed and eventually returns, the Rust Sequence will be resumed.

With any number of nested Lua threads and Sequences, control will always continuously return outside the GC arena and to the outer Rust code driving everything. This is the "trampoline" here, when using this interpreter, somewhere there is a loop that is continuously calling Arena::mutate and Thread::step, and it can stop or pause or change tasks at any time, not requiring unwinding the Rust stack.

This "stackless" style has many benefits, it allows for concurrency patterns that are difficult in some other VMs (like tasklets), and makes the VM much more resilient against untrusted script DoS.

The downside of the "stackless" style is that sometimes writing things as a Sequence is much more difficult than writing in normal, straight control flow. It would be great if async Rust / generators could help here someday, to allow for painlessly implementing Sequence, but there are several current compiler limitations that make this currently infeasible or so unergonomic that it is no longer worth it.

What currently works

What currently doesn't work

What will probably never be implemented

This is not an exhaustive list, but these are some things which I currently consider almost definite non-goals.

Why is it called 'piccolo'?

It's a cute little "pico" Lua, get it?

It's not really all that "pico", but it's still a cute little instrument you can safely carry with you anywhere!

Wasn't this project called something else? Luster? Deimos?

There was an embarassing naming kerfluffle where I somehow ended up with other people's project names twice. They're all the same project. I promise I'm done renaming it.

License

piccolo is licensed under either of:

at your option.