Thursday, May 30, 2013

Lua in JavaScript: Running a VM in a VM


Lua is a cool language, and it would be great to run it on the web. It isn't easy to do that though, see this answer (and the thread around it),
[Converting/running Lua in JavaScript] is a recurrent question on the Lua list, i guess because of the superficial similarity of the two languages.

Unfortunately, there are many important differences that are not so obvious. Making it work need either a full-blown compiler targeting JS instead of Lua's bytecode, or rewriting the Lua VM in JavaScript.
There are in fact projects taking those two approaches, for example lua.js and ljs respectively. But as mentioned in the quote above, it is quite difficult to get the full language (including things like using functions as indexes in tables, possible in Lua but not in JavaScript) with a simple 1-to-1 translation, and writing a full VM is not a quick solution either. Any such efforts miss out on all the work that has already gone into the Lua implementation in C.

There is also a third alternative, which is to compile the Lua C implementation into JavaScript, that is, to run the Lua VM inside your JavaScript VM.


lua.vm.js is a project I started over the last week that does just that, here is a benchmark page and here is a REPL for it.

The Lua VM compiles out of the box using Emscripten, with only a few minor Makefile tweaks. It's straightforward to then create a REPL where you tell the VM to run some code. lua.vm.js does more than that though, as you can see in the example on the REPL page, you can interact with the page using Lua,
local screen = js.global.screen
print("you haz " ..
      (screen.width*screen.height)
      .. " pixels")

local document = js.global.document
print("this window has title '" ..
      document.title
      .. "'")


local
window = js.global
window.alert("hello from lua!")
window.setTimeout(function()
  print('hello from lua callback')
end, 2500)
Lua is given a js global that lets you refer to things in the JavaScript/DOM world. You can get properties on those objects, call functions, even set callbacks back into Lua.

Since this uses the full Lua VM, it means you get the full Lua language, and it took just a few days since all the work that went into that VM is reused. That includes an incremental GC and everything else.

At this point you might be wondering if this isn't a crazy idea - a VM in a VM? There is definitely a lot of skepticism about that going around, but we won't know if the skepticism is justified or not if we don't try, hence this project.

The first specific concern is about size. It turns out that the entire compiled Lua VM fits in 200K when gzipped. That's too much for some use cases, but certainly acceptable for others (especially with proper caching).

Another concern is performance. That's what the benchmark page is for. As mentioned there, the Lua VM compiled into asm.js can have similar performance to other real-world codebases, around half the speed of native execution. Again, that is too much for some uses cases, but certainly acceptable for others. In particular, remember that the Lua VM is often significantly faster than other dynamic languages like Python and Ruby. These languages are useful in many cases even if they are not super-fast.

A third concern is compatibility. But compiling to JavaScript is the safest way to run everywhere, since it requires nothing nonstandard at all and consequently should run in all modern web browsers. Speed can differ of course, but the good thing about JavaScript performance is that if one browser is faster at something then the others always catch up.

So running a VM in a VM can make sense sometimes. It seems like an odd thing to run an entire VM on top of another, but the imagined overhead is not as big as it ends up in reality. If you have a lightweight, efficient VM in portable C, like Lua does, then when compiled to the web it behaves much like other portable C/C++ codebases, and it is possible to run compiled C code at near-native speeds on the web. JavaScript VMs are very capable these days, I think more than people sometimes assume.

How far can the approach of running a VM in a VM get us? Pretty far I think, this early prototype is further along than I had expected, and this is before any specific optimizations. There are however some tricky issues, for example we can't do cross-VM cycle collection - if a Lua object and a JavaScript object are both not referred to by anything, but do refer to each other, then to be able to free them we would need to be able to traverse the entire heap on both sides, and basically do our own garbage collection in place of the browser's - for normal JavaScript objects, not just a new type of objects like Lua ones. JavaScript engines don't let us do that, for a combination of security and performance reasons. What we can do is allow Lua to hold strong references into the JavaScript world, and automatically free those when Lua GCs such a reference. That limits things, but it's important to remember that cross-VM cycle collection is a hard problem in CS in general - the only easy case is when one VM's objects can be implemented entirely in the other, but that isn't possible in most cases (and not in this one: for example, some Lua objects can have finalizers - __gc - that can't be implemented in JavaScript) and even when it is, performance is a concern. But note that this type of problem would also be present if we shipped two separate VMs in web browsers.

Speaking of the idea of shipping additional VMs, that is a suggestion that is often heard, specifically I have seen people call for shipping Python, Mono, Lua, etc. VMs alongside JavaScript (alongside, because we need JavaScript comptability for the existing web, and none of those VMs can run JavaScript near the current speed, and we need to not regress performance). This approach has appeal, but also downsides. Perhaps the main issues are that when we compare it running other VMs in the JavaScript VM, then running in the JavaScript VM has some advantages:
  • Security is built in, since the new VM runs inside one that has already been heavily tested and hardened. The attack surface is not increased at all.
  • VMs are downloaded like any web content, which means we can use new languages as people port them to JavaScript, we don't need to wait for browsers to decide to support and ship additional VMs, and we don't risk different browsers deciding to support different VMs.
Running in the JavaScript VM also has disadvantages, but as discussed before they do not seem to be that bad in practice, at least for the Lua VM. It looks like the two approaches are architecturally very different, but can lead to similar results. So the bottom line is that instead of shipping additional VMs in web browsers alongside the JavaScript VM, we can run them inside the JavaScript VM, and the difference could be pretty much transparent to users: In both cases you can run Lua on the web, and in both cases performance is reasonable.



Maybe we wouldn't care? ;)

Last thought, what about running a really fast VM like LuaJIT on the web? That is trickier; LuaJIT has a JIT as well as a hand-written interpreter in assembly, so it can't just be compiled like portable C code can. The JIT would need a JavaScript backend, and the interpreter would need to be ported to JavaScript as well. In principle this could work, but it's hard to say how good performance would be, and there are some interesting questions about code invalidation in code JITed to JavaScript, PICs, etc. This is definitely worth trying, but it isn't a project of a few day's work like porting the Lua C implementation was.

In the meantime, if you love Lua, check out lua.vm.js. Feedback and contributions are welcome!

11 comments:

  1. Very nice.
    I'd like to point something at what B. Eich said recently at the conference. He said, that "maybe" the max asm.js speed could get to 1.5x native? Well, if this is the case, then you might want to have more time looking at improving the performance of emscripten, because PNaCl is comming at 1.15x. And yes, I'd prefer asm.js to take the lead, that's why I think you should focus more on performance at this stage.

    One other thing to look at would be the comming Nashorn project, with 100% compatible JS engine, aiming to run node.js applications at even better speeds than the current ones. (check the recent videos on the project)

    Main target of course should be C compatibility, but performance is also something worth looking more at it.

    ReplyDelete
    Replies
    1. Performance is definitely being looked at very carefully, I fully agree it's important.

      Firefox's asm.js optimizations can do even better than 1.5x, it will take work though, but in principle there is nothing PNaCl can do that it cannot. And note that already on some benchmarks it does better than 1.5x, for example Box2D is just 33% slower than native.

      The 1.15x number for PNaCl is also probably an average of various benchmarks, I am sure they do better on some and worse on others. Note though that AFAIK they generally ignore compilation time in their results, and the asm.js benchmarkj don't, so the results are not necessarily directly comparable.

      Delete
  2. I already write SVG pages in Postscript (Ps)
    run through Ghostscript.

    Firefox has PDF.js. PDF includes Ps.
    So, how does one run Ps through Firefox?

    Thank you,
    Eddie Maddox

    ReplyDelete
  3. Is it possible to include it in a website as a single .js file? As in can you just do script src="lua.vm.js" and then its good to go, or is there more to it?

    Also, whats the calling convention? Do you just write a lua code inside script type="text/lua" or do you call a js function etc

    ReplyDelete
    Replies
    1. This is possible with empydom: https://github.com/bkase/empydom/blob/master/srv/script-tag-example.html

      Delete
    2. Thanks! I'll take a look at how they do that.

      Overall the project is very new, thoughts on what the API should look like are welcome. I like the idea of using script tags.

      Delete
  4. These are good news!

    I've played around with lua.js, and, unfortunately, it seems to have a few disadvantages:

    * it does not support several Lua features (coroutines, for example, and maybe something else);
    * and it seems to require objects marshalling and unmarshalling (not a trivial problem in this case, imo, or I missed in lua.js too much re-implemented cyclic graphs objects traversing to marshall JS objects to lua.js and vice versa).

    My questions are:

    - How easy it takes to interop with JavaScript objects in this Lua implementation? (Yes, I've read the sample in the post, however, it would be great to know potential pitfalls)
    - Is it possible to disable js.global object access?
    - Does this implementation come with all features from the Lua standard library?

    Thank you very much in advance!

    ReplyDelete
    Replies
    1. Can you perhaps give an example for the things you found to not work? I'm not a lua expert so I'm not sure how to test those things just by their names.

      The only pitfall I am aware of is the cross-VM GC issue I mention in the post. But very possibly there are other issues we will run into.

      You can disable js.global of course, perhaps I should add a nice API call to make it even easier.

      This should already include all the standard libraries, I think - I followed the embedding examples as best I could. But I didn't test. Of course, what they can do is limited by the browser - you can't read files on the user's machine due to web security.

      Delete
    2. Thank you very much for the reply. Actually speaking, I'm not a Lua expert as well, however the author of lua.js described known issues in his implementations. Please see here: https://github.com/mherkender/lua.js#known-issues

      If I disable js.global somehow -- is it possible to restore it again from a Lua script? I'm just wondering if it can be considered as a security issue giving full access to the JavaScript globals, for example, sending unauthorized HTTP requests, etc.

      Delete
    3. If you disabled js.global, it won't be accessible in any way. That's the only way to reach out of the Lua VM.

      Delete
    4. Ok, understood. Thank you very much. :)

      Delete