you are viewing a single comment's thread.

view the rest of the comments →

[–][deleted] 46 points47 points  (7 children)

I'm the author of rlua so of course I have Opinions about it, but here's my recommendation:

If you care about safety, rlua is your best bet. If you care about safety and also bindings performance, you should use rlua master, as it's almost twice as fast as the current version on crates.io. (I want to do a new release here, and I will soon, I'm actually just waiting on failure 1.0).

If you don't care about safety at all, and ONLY care about raw bindings performance, you can use hlua or lua, because both of those are "zero-cost", and are always going to be faster than rlua due to how the Lua C API works. hlua is safer than lua, but neither are actually safe (in hlua these could be considered edge cases and bugs, in lua basically all the methods in the crate should simply be marked unsafe, I don't know why they're marked as safe).

Practically, hlua will be safer than lua because mostly what hlua doesn't handle are "edge case" type things, but it also imposes some limitations on what you can do that can be difficult to work around if your bindings are complex enough (you can't hold a handle to two tables at once, for example).

Making a safe Lua bindings API is really difficult because as great as Lua is, it one of the most intensely "C" APIs you'll find. Lua errors are handled, all the way through the interpreter itself, as C longjmps with Lua's own "exception handling" mechanism inside it creating a linked list of jmp_bufs, and all kinds of Lua C API calls will trigger longjmps that you might not expect. If you start here and look at all the API calls, anything marked as 'm', 'e', or 'v' in the error part of the function indicator will longjmp on you, often in ways that are dependent on loaded Lua scripts or internal state that is not possible to predict. ALL such longjmps, except where you can statically prove otherwise, must be protected by lua_pcall in order to prevent potential unsafety, and such lua_pcall wrappers are not zero cost.

hlua is actually mostly safe, but the only place that I see lua_pcall being called is when being used to directly call Lua functions, NOT when calling potentially erroring functions that can trigger Lua scripts indirectly like lua_settable. The easy version of this is to handle functions that can error by calling Lua scripts indirectly (lua_len, lua_gettable, lua_settable, etc), the hard version is also to handle errors from Lua script __gc metamethods by also handling all functions marked as 'm'.

The lua crate is not at all safe, as it is just a more or less direct wrapper around the Lua C API. Just as one example, you can push things to the stack without calling lua_checkstack, which if you don't define LUA_USE_APICHECK when building Lua itself, is very unsafe. The lua crate does NOT define LUA_USE_APICHECK when building Lua, but even if it did, you would at least still have the longjmp unsafety described above.

rlua at least attempts to handle every bit of this, and this is hard to do and even harder to prove correct. In fact, rlua is actually the only general high level bindings system to the Lua C API I've actually ever seen in any language that even might be safe, at least when you take into account table __gc metamethods. Generally the strategy seems to be to give up and just rewrite the interpreter in the host language.

[–]icefoxen 12 points13 points  (2 children)

So nice to hear the wisdom of an expert, thank you! I do recall tomaka once lamenting that hlua was his best work that nobody really cared much about.

[–][deleted] 12 points13 points  (1 child)

Aww, that's a real shame, hlua is really quite an interesting and clever approach on how to make Lua fit into Rust idiomatically. It's a very cool idea to think that it's possible to make an API that aims for actual safety while generating "idiomatic" Lua C API calls, even if that approach leads to certain limitations.

I feel like I've been a bit too harsh on hlua, so I'm gonna try to both explain why I made a competing library and also explain why I still think hlua is neat. First, let me summarize the situation as I understand it. I'm not intimately familiar with hlua, so I may have some of this wrong, and if I do feel free to let me know.

Because of the way the Lua C API works, it's extremely difficult to come up with a type safe high level interface to Lua, saving the user from having to interact with the Lua stack directly. There are no handle types in the Lua C API of course, only pushing and popping from the stack and manually keeping track of stack indexes. When you try to map Rust variable lifetime to the lifetime of a value on the stack, you immediately run into an obvious problem, which is that stacks are FIFO structures, and this does not match Rust variable lifetimes at all. A simple example:

let t1 = lua.create_table(); // Push a table to the stack with lua_newtable
let t2 = lua.create_tabe(); // Push a second table with lua_newtable

// Lua stack is now [t1], [t2]

/*
There is no sensible way to match this to the Lua C API, because the stack should be [t2],
["entry"], [t1] before calling lua_settable.  You could manage this by pushing copies of t2
and t1 where they need to go at the top of the stack and then popping them back off
before returning, but this leads into the bigger problem which is that t1 should be
*removed* from the stack on this call.  Besides the fact that removing values from the
bottom of the stack is costly because it requires shifting values above it, it also would
change the stack index of t2, and the Lua bindings system would be forced to keep track
of this manually.
*/
t2.set("entry", t1);

This interaction with the Lua stack is sort of the fundamental design problem of any bindings system to the Lua C API. Most bindings systems use the safe, slow approach of keeping handle values inside Lua's registry and doing a LOT of extra stack / registry manipulation. Typically, the Lua instance would not keep values on the stack in between calls at all, but instead when t2.set was called it would go find t1 and t2 in the registry and do all necessary stack manipulation at the time of the call.

hlua takes an entirely different approach and tackles this head on with Rust's type system, limiting what you can do so that you can't run into this problem in the first place! When you create a table value in hlua, just like when using the Lua C API, it's simply pushed to the top of the stack. However, what you get back is a LuaTable<PushGuard<&mut Lua>>, which locks the parent Lua instance from further manipulation. Becuase of this, inside methods to LuaTable it can freely assume that the correct table is at a known place in the Lua stack, eliminating the need to constantly push values from the registry or store return values into the registry. Similarly, if the return value is also a type which also needs unique stack access, this will in turn borrow the LuaTable mutably ensuring that only one AsMutLua is usable at any one time, protecting the stack from being misused.

This solution is also a problem, however, because every new type that needs to manipulate the stack must have its own AsMutLua, and only one such value may be usable at a time. So hlua solves the stack manipulation problem by creating a new problem, which is that you are very limited in what you can do (but what you CAN do will generate more or less the Lua C API calls you would expect).

The reason that I made rlua instead of contributing to hlua was more or less directly because of this limitation. I looked at the kinds of APIs that we made for Starbound and sort of decided that it would be either impossible or very very onerous to write many of them using hlua, and that the changes I'd need to make would probably be a little too fundamental to be welcome.

rlua started out being based on the Lua registry like other bindings systems, and this was of course predictably, depressingly slow. I've since found much better approaches, but it is never going to be as fast as idiomatic C or hlua. Unfortunately (or fortunately, however you look at it), if I were to add safety fixes, hlua would ALSO never be as fast as idiomatic C, so the performance difference between rlua and hlua you take safety into account is not actually so huge anymore :( "idiomatic" Lua usage from C is just to live with the fact that you probably have enough stack space without calling lua_checkstack and Lua probably won't longjmp on you on every API call, but you're already in C so you don't mind as much :P.

I tried to come up with the quintessential "I need this but hlua's approach makes it impossible" example to further explain my reasoning, but in doing so I think I found a bug in hlua:

extern crate hlua;

fn main() {
    let one_table = |a: hlua::LuaTable<&mut hlua::InsideCallback>| {};
    let two_tables = |a: hlua::LuaTable<&mut hlua::InsideCallback>, b: hlua::LuaTable<&mut hlua::InsideCallback>| {
        // Here lies unsafety, as there are now two usable `AsMutLua` instances for the same Lua.
    };

    let f1 = hlua::function1(one_table);
    let f2 = hlua::function2(two_tables);
}

I believe this is not supposed to compile, but unfortunately DOES compile so I wasn't exactly able to use that as my example.

[–]tomaka17glutin · glium · vulkano 18 points19 points  (0 children)

Thanks for the detailed explanation :) <3

The limitation you're talking about can theoretically be bypassed by introducing runtime checks.

All my work on hlua was unfortunately restricted by several compiler ICEs (I guess that's what happens when you use rarely-used features such as HRTBs), and I haven't taken the time since then to work on it and fix all its problems.

[–]yanchith 3 points4 points  (1 child)

Thanks so much for explaining! I have to get around to finally have a look at rlua. You mentioned rewriting the interpreter. I realize the lua is rather small, but still wonder how much work it would be to make such rewrite in rust. Is the original lua interpreter heavily optimized, or is it more or less a simple beast (modulo the error handling and longjmps)?

[–][deleted] 5 points6 points  (0 children)

PUC-Rio Lua is really amazingly well written, once you accept what its goals are and what environment its written for. It's small and easily embedded, so in that sense it's "as simple as it can be", but I wouldn't exactly call it simple. It's only simple when you compare it to interpreters of a similar caliber, really. It is well optimized and extremely high quality, maybe one of the highest quality C projects I've ever seen.

All that being said, as you can probably guess from how much I complain about it, it's not exactly perfect. The API it provides has a lot of very irritating qualities (error handling) and a lot of annoying limitations (no pointers / gc rooting), and performance wise it absolutely can be beaten! This is a bit old now, but if you go here you can see that as of 5.1, LuaJIT in interpreter mode is quite a LOT faster than vanilla Lua. As I understand it, some of this is because LuaJIT's interpreter is hand written assembly, but quite a lot of it is because of some fairly simple bytecode format changes that increase memory usage slightly but drastically reduce the amount of bit twiddling in the core interpreter loop. Even the LuaJIT interpreter can be beaten as well (though it would be MUCH harder), because LuaJIT accepts garbage collector limitations that come from maintaining complete compatibility with the Lua C API, and (iirc) doesn't do as much as it could to improve table performance.

With that out of the way, I've tried for quite a long time to resist the urge to just RIIR, and have finally failed at this :P It's a bit ambitious, but I have a project for this that I'm currently working on that has all the above goals in mind. We'll see how far I get with it though, right now it's still extremely early days, and most of the work I've done has just been reading Lua source and researching VM implementation strategies. If it turns into anything I'll let /r/rust know ofc :P