← Back to context

Comment by spacechild1

1 year ago

The Lua C API is fantastic from a design perspective, but as you have discovered, it is rather error prone. Fortunately, there are many wrapper libraries that take care of all the stack manipulation. For example, I'm using https://github.com/ThePhD/sol2 to interface with C++ and the API is incredibly ergonomic.

How is it fantastic, it’s just barebones stack interface thrown in your general direction.

Any integration code that doesn’t use an ad-hoc helper library looks like assembly listing (not even in disguise, cause it basically is bytecode but in C).

  • > it’s just barebones stack interface thrown in your general direction.

    I do not do a ton of low level development, but my impression is that you do not want a demanding interface. You want an interface that is straightforward, doesn't have complex pre & post conditions, and doesn't crash. Leaking 4 bytes per frame is bad but crashing would be worse! You may think you want a library that refuses to function at all unless its called perfectly, but once you're in the trenches you may change your mind.

    To my mind, it's a very good sign when there are popular "adapter" libraries. It means the underlying code (Lua) can be used many ways and people have found best practices for different approaches. When I used it I was not working on a game and it would have been worse for us if Lua's interface was "built for games." Being very bare bones and low level allows flexibility and that flexibility ensures a healthy ecosystem.

    •   #define T lua_gettop(L)
        …
        int t = T;
        …
        assert(T == t + expected_inc);
        …
        lua_pushvalue(L, -2); // note: this is b
        lua_remove(L, -3); // old b
        // eventual realization that
        // -index is fragile
        // and rewrite to
        int start = T;
        int a = ++start;
        int b = ++start;
        luaL_checkstack(L, 10, "…");
        …
        lua_pushvalue(L, b);
        lua_remove(L, b);
        lua_call(L, …);
        luaL_ref(L, …);
        …
        lua_settop(L, t - 1);
      

      If you have ever done this, idk how that feels flexibility. I’d rather get handles to a and b and directly call a function with them as like:

        LFN *func = LGETFN(L, LARG(1));
        LTAB *retvs = LCHKTAB(
          LCALL(L, func, 3,
            LGET(L, LARG(2)),
            LGET(L, LARG(3)),
            LGET(L, LUPV(1))
          )
        );
        me.retvs = LREF(L, retvs);
        // everything but ref’d
        // things disappears here:
        return;
      

      And avoid all this stack-in-your-mind and stack-in-your-int nonsense, which is completely unnecessary in C and I fail to see where exactly it is flexible, compared to the short snippet above. Stack is just how vm sees it, it is not any special “for you” thing, it’s literally implementation shining through.

      I’d like to see a specific example (low-key, I left Lua) on how its stack provides more flexibility or anything, because in my humble experience it does not, absolutely, do anything like it. It just makes you play these off-by-one games all day for no good reason.

      1 reply →

  • > How is it fantastic

    See my other reply: https://news.ycombinator.com/item?id=42519033

    > Any integration code that doesn’t use an ad-hoc helper library

    There are mature bindings for most languages. You only need to deal with the C API directly when you

    1. Create a new language binding

    2. Write a Lua extension

    3. Call Lua from C

    Typically, Lua is embedded as a scripting or configuration language into another project, and in this case you wouldn't even be aware of the C API.

    • The problem is, if we are talking about Lua API, typically you are on the other side of “embedded in another project”, as an embedder. My reply shortly above goes into some details.

      (from elsewhere) It is a low-level API for creating bindings to other languages and as such typically isn't used directly. (Well, unless you are writing C :)

      Yeah, unless. But even under this assumption, wrapping handles to values would be much more easier in a language-specific wrapper, because it wouldn’t have to watch out for stack overflows etc and solve the exploding stack problem in general in naive api-user-side loops. This thread started as Lua stack API is better than Python’s, iirc. But Python API already looks like a high-level wrapper doing exactly that, so… what was the point again?

      I’m a little concerned that everyone talks high matters here without providing examples or at least looking into “final particles”. Cause that’s where claims get tested against reality. I spent too many years in deep Lua and its mailing list to miss something obvious that everyone knows.

      3 replies →

An API cannot be “fantastic” if it’s error prone.

  • It is a low-level API for creating bindings to other languages and as such typically isn't used directly. (Well, unless you are writing C :)

    What I find fantastic about the stack-based API is that it exposes very few interpreter internals. As a result the authors may drastically change the implementation without breaking the public API. Most importantly, they hide the internal object representation and as a user you only ever deal with plain C types, like integers, floats, C-strings and user data pointers. As a consequence, you don't have to think about memory management because all types are passed by value! In fact, someone could theoretically write a Lua implementation with reference counting and the API wouldn't have to change. It really is a very smart and elegant design.

  • It might be fantastic in other ways; there are multiple ways an API can be good. For example, it could be very expressive and flexible, or it could be especially amenable to formal proofs with things like TLA+ even if it's error-prone in human hands, or it could facilitate very high performance. Even on the error-proneness axis, an error-prone API might still be less error-prone than the alternatives.

  • You can only make a C API for interacting with a precise garbage collector that ergonomic. And, to be honest, I wouldn’t call the situation described by the GGP particularly problematic—you messed up memory accounting and you got a memory leak. In most garbage-collected runtimes, if the runtime messes up memory accounting, at best you get immediate memory corruption, at worst you get memory corruption much later in some unrelated part of your heap. So this is pretty mild all things considered. Of course, if you’re working in a language with greater metaprogramming capabilities than C, absolutely do abstract the stack into something more manageable.