Agreed, Lua seems to be the de-facto standard here, as it's so lightweight to embed. But the language has some pretty janky rough edges (e.g. only having Tables and no other data structures) so a modern alternative would be nice.
Yes, Lua tables are incredibly versatile tool. I'm not a fan of global-scope-by-default and of verbosity, but other than that it's a fantastic language.
Global scope by default isn't good practice in embedded. Global scope isn't synonymous with static allocation, but unfortunately that goes over the head of many people. Best practice in embedded is to have locally scoped statically allocated data passed around by reference.
It really depends on the application. For smaller, more targetted embedded applications, global scope is fine. If every function needs to have access to a common set of variables, there's really no need to be passing references around.
Even if the application is tiny, that's no excuse. It makes it even easier to chuck everything into a single struct type. It's not like there's any performance gain to having globally scoped stuff on a modern compiler compared to passing references.
One reason using global variables is bad practice is it makes testing harder. An unfortunately high percentage of embedded software doesn't have any sort of harness-based testing because it's written with globals spammed everywhere, which prevents you from using any kind of principled testing strategy where you mock out all the hardware dependencies. It's especially bad if there's globally defined MMIO stuff like "#DEFINE CCR1A (uint64_t)0x74FEA10". Good luck testing that!
Smaller embedded targets don't have modern C++ compilers. Also many engineers want to solve domain problems instead of dealing with C++ related problems.
In domain of C, passing by reference means passing pointer. If you chuck everything into single struct and pass by pointer, it has same problems as global scope.
Not that I'm advocating for global variables. Even tiny projects tend to grow with time, and localizing scope across the code base is not fun at all. In context of Lua, I've just trained myself to prefix variables with 'local' and I don't give it much thought.
> If you chuck everything into single struct and pass by pointer, it has same problems as global scope.
Not true. In fact, this refactor is one of the best things you can do to improve an old shitty embedded C codebase. Among other benefits, it allows you to have multiple instances (an arbitrary and easily-adjusted number, in fact) of a system sharing the same memory, reduces the complexity of linker-related BS, and simplifies testing. It's vastly better than relying on horrendous C cross-module scoping rules for sharing.
To me the main problem of global is that any module can mutate and affect any other part. All encapsulation and modularisation is then leaky and you are always on your toes about implementation details in some other part of code. Your approach does not attempt to solve this downside of globals.
I agree that passing in structs is vastly better than communicating over globals. On the other hand, taking existing code base and implementing this state-passing is quite large undertaking that affects all function declarations/implementations. It might be beneficial, but there are often better investments of your time.
A lot of people who don't like Lua list "only having tables" as some sort of negative, but honestly I don't see how that is bad in any way.
1. Having only 1 data structure makes for a simpler, more elegant language. Once you understand tables, you understand everything you need to know about lua.
2. You could implement any other data structure with a table.
* A table is basically a dictionary / map already
* A table can be an array if you use numbers as keys. They don't even have to start at 1.
* A table can easily be made into a set if you use the elements of the set as keys and 'true' as the values.
* A table can be made into a proper OOP class with metatables
* A table can use prototypical inheritance too
* A table can act as a basic data record object
Those using the language don't really miss having the more specific data structures.
In my experience, it took a bit of conceptual work to get used to the "Lua way", and that impedance mismatch would have been reduced if it shared more similarities with the other languages we were working with.
Context is important IMO; if you're going to using a language as your primary development tool, then you'll get over that hump fairly quickly, and my objection is less relevant. But for our use-case (embedding Lua in a C application to have user-provided scripts drive our C library's callback hooks), the developers were primarily working in C and Python, and so most of us didn't use Lua often enough to really click with it.
For our use-case, something a bit closer to Python or Java would have been much easier to grok, and therefore would have made our development easier and more productive. "Easy and more productive" is all I'm really looking for in a language, always within the context of the specific usecase of course.
I'd be fine with dropping most of the sugar in Python or Java, but I'd be surprised if there were no "zero-cost abstractions" that could be added to Lua without making it too heavyweight for embedding.
There are a lot of alternatives. See https://github.com/hengestone/lua-languages. These languages use the Lua or LuaJIT VM as a backend, either as a Lua transpiler or a bytecode compiler.