DEV Community

Cover image for Lunatic: Actor based WebAssembly runtime for the backend
Bernard Kolobara
Bernard Kolobara

Posted on

Lunatic: Actor based WebAssembly runtime for the backend

I would like to share with you a project I have been working on for the last few months. Lunatic is a WASM runtime heavily inspired by Erlang and Go. I tried to take all the cool parts of these VMs and extend them with a few new features that are interesting to me. Mostly I focused on having a great VM for low latency networking applications. It's still not public, so you will need to be a bit patient before trying it out.

The main features are:

  • Actors are represented as WebAssembly instances.
  • Sandboxing and runtime permissions are on a per actor basis.
  • All blocking code is transformed into asynchronous automatically.

It's written in Rust using Wasmtime and Tokio, plus a custom stack switching implementation inspired by libfringe and an async stdlib implementation compatible with WASI.

How does everything fit together?

The architecture of Lunatic is quite simple. Tokio is used to schedule lightweight tasks (actors) consisting of WASM instances. Each instance is self contained (has its own stack and heap) and sandboxed, but still lightweight enough so I can spawn 300k/s tasks on my almost decade old MacBook.

In this case you can just have your web app spawn a new WASM instance for each request. Opposite of similar runtimes (Go/Erlang) you have per actor precise permissions. You can only allow files being opened from a specific folder, forbid outgoing network requests or limit memory usage. You can also use safely any existing C library inside the actor, knowing that any vulnerability or crash will just be limited to this particular request.

From the application's (that runs on top of Lunatic) perspective, all operations are blocking. You write your regular old non-async rust or C++ code, but once you compile it to WASM and run on top of Lunatic, all the blocking calls are transformed into async alternatives. Your code is simplified because you don't need to use async notations and can mix in existing C libraries with blocking syscalls, but the runtime will take care of using async alternatives instead and suspending the actors.

Having 2 million connections in your Rust application, but not needing to reach out to async code should be a nice experience :).

Using WASM as the bytecode allows Lunatic to be targeted by any language that compiles to WASM, including Rust, C/C++, Go, AssemblyScript and others. It is also possible to write different actors in different languages.

Low latency

One issue that is common across async Rust, Erlang and Go is that to assure low latency the runtime can't spend too much time on a single task. You need to yield back from this particular task for the system to re-schedule it. Or it will end up blocking a thread for a long time. This is easy if you stay inside the particular language, but really hard if you call out into C code. The advantage of Lunatic here is that all code is compiled to WASM, even C extensions and it will always periodically yield back, even if you have a tight infinite loop.

Future development

Originally I started working on a programming language, but realised I could not accomplish the things I wanted with existing VMs. So I ended up developing another runtime.

It's still early in development and I'm constantly experimenting with different features before settling down for a final design. Lately I have been playing with implementing the threading part of the standard library so you could just spawn new threads from your existing language but they will actually be abstracted as actors in Lunatic.

If you want to follow along with the development, I intend to tweet now more frequently about it.

Top comments (1)

pocheptsov profile image
Slava Pocheptsov

Sounds interesting and conceptually similar to Motoko by Andreas Rossberg.