Hacker News new | ask | show | jobs
by vvanders 3333 days ago
Great stuff, lines up a lot with what I'd care about as an ex-gamedev and spending a bit of time with Rust. One minor point:

> First class code hot-loading support would be a huge boon for game developers. The majority of game code is not particularly amenable to automated testing, and lots of iteration is done by playing the game itself to observe changes. I’ve got something hacked up with dylib reloading, but it requires plenty of per-project boilerplate and some additional shenanigans to disable it in production builds.

Lua is a great fit here and interops with Rust(and just about everything else) very well.

2 comments

A long time ago I wrote bindings to Lua 5.1 (https://github.com/kballard/rust-lua), but there was a nasty problem where Lua uses longjmp() for errors (it can be configured to use C++ exceptions, but that doesn't work because you can't throw those past an extern C boundary without hitting undefined behavior). longjmp(), of course, will just skip right past the intervening stack frames, meaning if your Rust code calls into Lua and it throws an error, then unless you wrapped that call with a lua_pcall, any values that live on the stack of your Rust function will never call their destructors.

I admit I haven't bothered to research the current state of things, but how do more recent Lua bindings handle this? Does Lua 5.3 actually have a proper solution here, or do most bindings just wrap every single call with lua_pcall? I didn't do that in my bindings because I wanted to offer the full speed of Lua, but it's certainly an option.

The typical solution in C is to wrap and anchor such objects in the Lua VM so that they'll be garbage collected if an error is thrown. There are various patterns for doing this--direct binding of individual objects, or indirectly anchoring through an opaque staging struct--but that's the general idea.

Because Lua supports coroutines with a stack independent from the system C (Rust) stack, you often want to be careful mixing your stack-allocated objects. Lua 5.2 added lua_callk which allows yielding and resuming coroutines across the C API (that is, "yielding" a coroutine with a nested C or Rust function invocation).

Leveraging Lua's awesome coroutine support is one of the biggest reasons to use Lua, IMO.

Also, Lua can safely recover from out-of-memory (OOM) scenarios. On OOM it will throw an error that can be safely caught. Any Lua API which might internally allocate memory can throw this error, not just the lua_call family of routines. Quality libraries are expected to also handle OOM, which usually can be accomplished the same way as handling any other error: wrapping and anchoring temporaries, directly or indirectly, in the Lua VM.

That's still pretty much the state of the world. I think that's a trade-off that makes sense as long as you're aware that you shouldn't be depending on drop functionality in callbacks(I would hope that your initial point is dostring/dofile and let that catch handle things).

Generally I've found that any time you're interacting with an FFI in Rust all bets are off and you need to be very aware of what your libraries do and what their runtime looks like(just in C/C++).

I had intended to write a compiler plugin for rust-lua that added a lint that would ensure you have nothing on the stack with Drop when you call a (potentially-error-throwing) Lua function. But I never got around to doing that because I stopped using rust-lua (I canned the one project I was doing that motivated rust-lua in the first place).
Lua is great and generally the right tool for this sort of thing.

As an alternative, though, there are several Rust-based scripting languages that attempt to expose some of the power of the type system, etc., while being more amenable to dynamic loading, REPL, etc:

http://libs.rs/scripting/

One that's not listed there but might be a good candidate for a Rust project is miri [1] since it's both interpreted and still Rust. It's still pretty rough, but the approach it takes should perform pretty well and won't require people to learn a new language.

[1] https://github.com/solson/miri

It is outrageously fast (at least compared to compile) but would need integration via libffi to speak to the rest of the universe. This is not a priority for the maintainer, since he's exploring compile-time metaprogramming (where you want a very restricted sandbox)
Dyon (from that link) looks very cool! I'd also like to add that JavaScript is another language used in this arena, and there is a crate with V8 bindings (https://crates.io/crates/v8). Even Garry Newman (creator of Garry's mod) wrote that he believes JS would have been better than Lua for scripting: https://garry.tv/2014/08/16/i-fell-out-of-love-with-lua/
Unfortunately, some of those syntax niceties the Garry's mod creator mentioned are things that have been identified as leading causes of bugs.

++ mutates a variable in place, has non-trivial pre/post behaviour (many junior devs don't understand it), and its brevity causes it to be used inline, which results in complex one liners.

String concatenation using '+' is not considered to be a great feature in dynamically typed languages. When you can't guarantee the types of the operands, it can lead to unexpected results. Pass in two numbers, and it this returns the result of the sum. Pass in two strings, and they're concatenated.

'continue' is a strange one. I just don't use it, and I'm not sure why anyone thinks it adds anything. It's like an early return, in that it can cause some difficulty in code comprehension.

So it feels a little like Lua is being criticised for making some good calls.

> String concatenation using '+' is not considered to be a great feature in dynamically typed languages.

Yes, one of the things Perl got very right, and yet it is still brought up by people as evidence of what makes Perl look like line noise (along with == vs eq, which is the same thing, even if the problem is less troublesome in that case).

> So it feels a little like Lua is being criticised for making some good calls.

All too often languages (and aspects of them) are criticized mistanely as being worse when all that is presented is how they are different. All too often we vilify the unusual just for being unusual.

As to 'continue', starting with Lua 5.2 you can actually simulate it perfectly with a

  goto continue
and then a

  ::continue::
slapped immediately before the relevant loop's end. Surprisingly, it's not affected by the rule forbidding "going into new variable scope" - because "the scope of any variable ends before 'end', wink wink, nice trick we left here for you no?"
Yeah, Lua certainly has it quirks. However binding to JS is a huge endeavor. For instance that v8 crate doesn't easily expose a way to call Rust from JS which is trivial in Lua.

Also the JS runtime is huge and the build system is complex. You can run Lua in ~400kb or less and LuaJIT tends to walk over any other jitted interpreted runtime.

While the performance definitely doesn't compare to LuaJIT, Duktape (http://duktape.org/) could be a good target for Rust bindings - it's fairly small and available as a single-header/implementation library.
He even invoked Wadler's Law!

It really is too bad that JavaScript is different from Lua. Lua predates it by 2 years. [1]

Criticizing a language on syntax is something I would hope we could move past. Mr Newman didn't read about the history and purpose of Lua, otherwise he would know it was targeted at scripting data loads for simulations written in Fortran. Hence the 1 based indexing. Lua syntax is one of its SELLING points.

[1] https://www.lua.org/versions.html

It's really too bad we don't use Lua in the browser...