This article is written by Maksim Lin
Welcome to another edition of #FlutterFunFriday! In this instalment we’ll be learning how to use Lua in our Flutter apps. So grab a beverage of your choice, fire up your favourite IDE and lets have some fun!
Lua ?
Lua is a small scripting language that is widely used in games, from small hobby projects all the way through to big, well known commercial titles. As more games are written in Flutter, the need for a scripting language for games made in Flutter also grows.
Making use of Lua in Flutter apps is possible in a few different ways, but the specific one I want to cover here is by using the LuaDardo package, an implementation of a Lua 5.3 compatible LuaVM written in pure Dart.
The OG
The reason why this article even exists is because while the original Lua VM in C was meant to be easy to embed in C programs, the embedding API, especially the way to call Lua from its host application and for Lua to call into its host is both not immediately obvious nor is it’s Dart specific variant documented in the LuaDardo project. Luckily for us, LuaDardo uses pretty much the same embedding API as does the original C version of Lua and its API is well documented in the “Lua Book”Programming in Lua the first edition of which is available online.
So “all” that is left for us to do is to translate the C-based API into it’s equivalent Dart implementation provided by LuaDardo.
The Lua Stack
We start with the most important concept in interfacing with Lua, the Lua stack. The stack is the means by which we can send and receive data to and from Lua. The Lua API provides us with various functions (methods in LuaDardo) of manipulating that stack.
As the name suggests, the stack is a data structure that allows both the host code (in this case our Dart code) as well as the LuaVM implementation the ability to push and pop data items onto and off the stack as shown in the diagram below:
The details of what and when things get pushed and popped from this stack we will cover with examples in the rest of this article.
Getting started
To begin with, lets start with the simplest example of just being able to load and initialise a Lua script using LuaDardo. This involves first initialising the LuaVm’s state:
final state = LuaState.newState();
then we can use this state object to load the Lua code (a “chunk” in Lua parlance) from a string:
final code = "print('hello from Lua')";
state.loadString(code);
and then we can cause Lua to evaluate that code chunk:
state.call(0, 0);
However doing so will cause us to have an exception be thrown:
Unhandled Exception: Exception: not function!
This helps illustrate an important point with using Lua, by default no Lua standard library functions are loaded into the newly initialised VM, not even print()
!
We could fix this by calling:
state.openLibs();
before call()
which would cause the VM to load LuaDardos standard library function implementations including print, but I consider not having any standard library functions available a very good feature of using Lua, especially if you intend to use it to run user supplied, untrusted code! As doing so means there is much less chance that users code can break out of the Lua sandbox as only the functions that you explicitly provide will be made available.
Calling from Lua into Dart
Following the above course, we can now provide an implementation of the print()
function to our Lua script by calling the method to register the Dart function with the state:
int _printFromLua(LuaState ls) {
final mesg = ls.checkString(1);
// now we have them, pop args off the Lua stack
ls.pop(1);
debugPrint("[LUA] $mesg");
return 1;
}
...
state.register("print", _printFromLua);
Note that all functions that are registered with the state object must implement the function signature of: int Function(LuaState)
as shown above.
In the simple case of the print function above, the Dart implementation uses 2 methods: checkString()
and pop()
which will allow us to examine now the details of using the Lua stack.
So what happens when we evaluate the Lua chunk and hence the global call to print()
occurs? Well firstly the LuaVM will push
the argument to the print
function, in this case the string ‘hello from Lua’ onto the stack:
The LuaVM will then call back into the Dart function we registered for the name print
. The LuaState
object that gets passed into the callback to printFromLua()
essentially represents the Lua stack. So when we call:
final mesg = ls.checkString(1);
The checkString()
method, we pass it the index of the item we want to read from the stack, in this case index 1
. Lua also provides a way to use indexes relative to the top of the stack, decrementing down, which is often a more convienent way to address items on the stack, so in this case we just as easily have used -1
as the index to represent the item at the top of the stack (and hence -2
would be the 2nd top-most item on the stack and so forth).
As the name might suggest, checkString()
not only reads and returns an item from the stack, but also sanity checks that the item has the expected type, in this case a string.
However, just reading the item does not manipulate the stack in any way, so that item remains on the stack afterwards. But Lua requires us to clean up after ourselves, so before our callback method completes, we want to remove the item we have used hence the call to pop it off to clear the stack:
ls.pop(1);
This cleanup is essential when dealing with Lua, as it is quite likely that the state could continue to be used as the script continues to run and more Lua code is executed and I have found a common source of errors when using LuaDardo is forgetting to tidy up after having made use of the stack. Luckily LuaDardo provides a very handy extension method to print to the Dart/Flutter debug console the current state of the stack:
ls.printStack();
Calling from Dart into Lua
Now that we have the Lua code loading in the LuaVM, being evaluated and calling a function implemented in Dart code, lets look at going the other way and having Dart call a Lua function.
To invoke a Lua (global) function from Dart, what is required is to first “lookup” the function in the Lua VM, for instance if we want to invoke a function called onUpdate
:
const funcName = "onUpdate";
final t = state.getGlobal(funcName);
We should always check that the found symbol actually is a function:
if (t != LuaType.luaFunction) {
print("Lua type error, expected a function but got [$t] ${state.toStr(-1)}");
return;
}
And then to invoke the function it is:
final r = state.pCall(0, 0, 1);
though again we should always make sure we check for any error conditions:
if (r != ThreadStatus.lua_ok) {
print("Lua error calling $funcName: ${state.toStr(-1)}");
return;
}
Table stacks: passing array data from Lua to Dart
While the technique we covered above works for single pieces of data, what about if we need to send larger amounts of data, for example arrays? This is also possible with the Lua API, but is a bit unintuitive, so I will cover it here as a more advanced use of the Lua stack.
Unlike in some other languages, Lua treats arrays as a special case of its more general Table
data structure (in fact almost everything in Lua turns out to be implemented as a Table!)
To illustrate passing a array from Lua to Dart, we’ll use an array of Integer
’s that might for example represent a simple bitmap we may wish to display on the Flutter side.
First off we need a piece of array data on the Lua side which we can send to Dart by calling a method which we will name simply draw()
:
local data =
{0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
0, 1, 1, 1, 0,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1}
draw(data)
In Dart, we can “wire up” this draw function as we did with the print function earlier:
state.register("draw", _draw);
And then in our Dart _draw
method we define the expected function signature, check that the first item on the Lua stack is a table type as expected and that we can get a valid length for the table:
int _draw(LuaState ls) {
// first check that the top of the stack is a table
if (!ls.isTable(1)) {
ls.printStack(); //debug: print out current Lua stack
throw Exception("Did not find expected Table data from Lua on stack");
}
final bitMapDataLength = ls.len2(-1); //get size of table
if (bitMapDataLength == null) {
throw Exception("Did not find expected Table length from Lua on stack");
}
...
Once we have that we can continue on to the tricky bit of calling getTable()
method to access each item in the array (aka the table):
for (int i = 0; i < bitMapDataLength; i++) {
// get i-th element of the table
ls.pushInteger(i + 1); // +1 because Lua uses ONE not ZERO based indexing!
// getTable() will use the value on the top of the stack to lookup a value in the TABLE thats at -2
// on the stack and then replace the value that was on the top that looked up value from the Table
ls.getTable(-2);
if (ls.isInteger(-1)) {
// Get the value of key k1
final d = ls.toInteger(-1);
bitMapData[i] = d == 0 ? false : true;
ls.pop(1); // now pop the value we just got from the table
} else {
throw Exception("invalid Table item type, expected integer");
}
draw(bitMapData);
}
This is probably the hardest to understand parts of the Lua API we cover here, so I have left in the comments in the code sample above. But it still may need some further explanation of how it works here. Essentially what happens is a 3 step process:
1 We push the index of the item in the table that we want to access onto the stack (eg. 1
to access the first item, because Lua uses 1-based, not 0-based indexing).
2 We called ls.getTable()
with the an argument of the index that the table has on the stack, which is now -2
because we just pushed the 1
integer onto the stack. The call to getTable()
causes Lua to A.) pop the first item off the stack (again the 1
that we pushed in step 1) and then push onto the stack the value of the item in the table that was referenced by that value that had just been on the stack, hence the stack now contains the value of item 1
from the table as the top most item on the stack.
3 Finally we get the value of the top most item on the stack with ls.toInteger(-1)
and then ls.pop()
to remove it from the stack to be ready to do the steps all over again for the next item in the table (hence why we do this in a Dart for
loop).
Now I admit the above is quite a mouthful and maybe easier to understand in diagram form below:
Summary
With that, our foray into the world of using Lua in our Flutter apps is over. Hopefully this has given you a taste for how you can make use of Lua to add scripting to your own Flutter apps or Flame games. Please tune in again for the next exciting installment of #FlutterFunFriday and in the meantime, keep popping those stacks!