ShiVa Lua unlocked Pt.4: code injection, garbagecollector, coroutines – ShiVa Engine

ShiVa Lua unlocked Pt.4: code injection, garbagecollector, coroutines

Welcome to the probably last entry in this tutorial series. Today, we are going to have a look at the last remaining undocumented RealtimeAPI functions, which allow you to load arbitrary external Lua code, control the garbage collector, and let functions run seemingly in parallel.

Code injection

With Lua being an interpreted rather than a compiled language, it allows you to introduce code into your application at runtime. From one liners to entire file libraries, it's all possible thanks to loadstring(), loadfile() and dofile().

loadstring()

loadstring() is a function that takes a string as an argument and executes it as if it were Lua code as soon as you add a pair of (). This allows you to construct new Lua code on the fly.

  1. local f = loadstring("local a = 10; return a + 20")
  2. -- execute code through () syntax
  3. log.message( f() )
  4. -- returns 30

loadstring() is always executed in a global context. You will not be able to access local variables and functions. If you wish to interact with data in your ShiVa function, you need to make all relevant data global by omitting the "local" keyword:

  1. -- global variable a
  2. a = 5
  3. -- loadstring will use the global a for its calculation
  4. local f = loadstring("a = a + 5")
  5. -- execute f
  6. f()
  7. log.message ( "a is now " ..a )
  8. -- returns 10

An easy way to evaluate a loadstring() expression is through the "return" prefix. Imagine the formula "a = a + 5" as a user input in a math test game; the example block from above could also be written like this:

  1. -- still global variable a
  2. a = 5
  3. -- loadstring will use the global a for its calculation
  4. local f = loadstring("return " .."a = a + 5")
  5. log.message ( "a is now " ..f() )
  6. -- returns 10

The loadstring() function is powerful, but comes with a major drawback: it is quite an expensive function when compared to its alternatives and may result in incomprehensible code through the use of global variables. Before you use it, make sure that there is no simpler way. Most of the time, loadstring() is used to construct new variable names or introduce specialized version of functions. If you use ShiVa tables and function parameters instead, you can most likely completely avoid loadstring().

dofile()

Instead of loading long strings, it is often easier to load an entire file with all your external Lua code. This can be done through dofile(). In addition to sending data to and from ShiVa using global variables, you can also define functions inside external Lua files, which makes writing libraries and including them with dofile() a viable, if not that elegant, option. let's consider this example file:

  1. -- file: executeMe.lua
  2. log.message("file received:" ..num)
  3. num = num + 6
  4.  
  5. function fileSquare(nNum)
  6. return nNum*nNum
  7. end

The corresponding ShiVa script could look something like this:

  1. -- define global variable num
  2. num = 12
  3.  
  4. -- execute an external file, and load its functions
  5. local l = dofile("Z:\\executeMe.lua")
  6. -- this will immediately log "file received: 12" as num was defined globally
  7.  
  8. -- then num = num + 6 will be executed and its result 18 will be available to ShiVa
  9. log.message ( num )
  10.  
  11. -- finally, we call a function that was defined in the external Lua file
  12. log.message ( "square in file: " ..fileSquare(num) )

Note that all functions loaded with dofile() are in the global context, which means they can be called from any script in your game.

Garbage collector

Lua is a scripting language that ships with an automatic memory manager known as garbage collector (GC). That means, unlike other languages like C and C++, you do not have to concern yourself with purging memory of objects, pointers, variables etc. that are no longer in use, so memory leaks can be prevented. This has both advantages and disadvantages. Fortunately, there are ways you can take manual control of the GC, in case the automatic settings make your code slower than it should be at the expense of increased memory usage.

Runtime GC

Lua 5.0.x, which is used in the ShiVa engine runtimes, has only very limited options for the garbage collector. The function gcinfo() returns the current memory usage, as well as the threshold for the garbage collector. When the number of bytes used crosses the threshold, Lua runs the garbage collector, which reclaims the memory of all dead objects. The byte counter is adjusted, and then the threshold is reset to twice the new value of the byte counter.

  1. -- only runtime engine
  2. local memUsage, threshold = gcinfo()
  3. log.message ( "Lua Memory usage in kB: " ..math.trunc ( memUsage, 1 ) )

You can force a GC cycle through the function collectgarbage([limit]). If the optional [limit] parameter is smaller than the byte counter, then Lua immediately runs the garbage collector.

Editor GC

Luckily, the ShiVa 2.0 Editor uses Lua 5.2.3, which gives much more control over the garbage collector. Everything is now controlled by the function collectgarbage([opt]) and its new list of parameters:

"collect": performs a full GC cycle (default)
"stop": stops GC
"restart": restarts GC
"count": returns Lua memory usage in kb

There are even more options, but those are the most important ones. If you want to know more, please refer to the official Lua 5.2 docs. This is how checking for the current Lua memory usage looks like, not how the memory usage changes when you put a variable on the stack:

  1. -- more modern version
  2. local memUsageModern = collectgarbage ("count")
  3. log.message ( "Before: " ..math.floor ( memUsageModern ) )
  4. -- now load something into memory
  5. local a=12
  6. memUsageModern = collectgarbage ("count")
  7. log.message ( "After: " ..math.floor ( memUsageModern ) )

If you have very expensive loops with large objects, it can be beneficial for performance to temporarily disable the GC. don't forget to turn it on afterwards though!

  1. -- our "expensive" function
  2. function f(y)
  3. local x = (y*y + y ) / 2
  4. return function() return f(x) end
  5. end
  6.  
  7. -- disable GC
  8. collectgarbage('stop')
  9.  
  10. local g = f(0)
  11. -- long loop
  12. for i=1,10000 do
  13. g = g()
  14. end
  15.  
  16. -- do some checks
  17. log.message("garbage after FOR call:" ..collectgarbage("count"))
  18. -- force full GC cycle
  19. collectgarbage()
  20. -- check if memory is clear again
  21. log.message ( "After GC: " ..collectgarbage ("count") )
  22.  
  23. -- continue regular GC operation
  24. collectgarbage('restart')

Example courtesy of luatut.com.

Coroutines

Lua does not have true multithreading (yet). Instead, if you want to run functions seemingly in parallel in Lua, you have to use coroutines. A program with threads runs several threads concurrently. Coroutines on the other hand are collaborative: A program with coroutines is, at any given time, running only one of its coroutines and this running coroutine only suspends its execution when it explicitly requests to be suspended.

Why then would you want to use coroutines in your game if it does not use more cores, if it does not really speed up your game? Because you can distribute your workloads better, producing a smoother experience, making your game feel faster. For certain workloads like loading in a large table of objects, textures or other gamefiles, coroutines are the tool of choice.

Concept

In order to adapt coroutines for ShiVa, we will have to understand the basic concept first. Consider two functions:

  1. function f1()
  2. local i = 0
  3. while i < 200 do log.message("co1_"..i); i = i + 1 end
  4. end
  5.  
  6. function f2()
  7. local i = 0
  8. while i < 200 do log.message("co2_"..i); i = i + 1 end
  9. end
  10.  
  11. -- execute sequentially
  12. f1()
  13. f2()

If we just call them one after the other, we get a sequential output of co1_0..199 and then co2_0..199. Futhermore, the entire engine stalls until all 2x200 iterations are computed. For a small example like this, you will see a bit of stutter, but imagine doing this with a large array of game models - the game will grind to a halt for presumably several seconds.

The coroutine.*() API has 3 main functions:

"create": transform a function into a coroutine
"yield": pause a coroutine, keep the current state
"resume": starts or resumes a coroutine until a yield() is encountered

Let's transform both example functions into (anonymous) coroutines and define a yield point after every iteration of the expensive while-loop. Note once again the global context:

  1. co1 = coroutine.create(function ()
  2. local i = 0
  3. while i < 200 do
  4. log.message("co1_"..i)
  5. i = i + 1
  6. coroutine.yield()
  7. end
  8. end)
  9.  
  10. co2 = coroutine.create(function ()
  11. local i = 0
  12. while i < 200 do
  13. log.message("co2_"..i)
  14. i = i + 1
  15. coroutine.yield()
  16. end
  17. end)

To execute these coroutines, we need to use resume(), which will run the while loop exactly once until the yield() is encountered. Running resume() again will compute the second interation of the while loop, and so forth.

  1. coroutine.resume(co1)
  2. -- logs "co1_0"
  3. coroutine.resume(co1)
  4. -- logs "co1_1"
  5.  
  6. -- how about the other one?
  7. coroutine.resume(co2)
  8. -- logs "co2_0"
  9.  
  10. -- back to the first one
  11. coroutine.resume(co1)
  12. -- logs "co1_2"

Of course, executing long loops manually one by one is not the answer. But neither are for-loop, mind you:

  1. for i=1,300 do
  2. coroutine.resume(co1)
  3. coroutine.resume(co2)
  4. end

A loop like this will simply execute the coroutines sequentially again in one frame until all 2x200 iterations have finished. To make coroutines work with ShiVa, we will have to adapt our code slightly.

Coroutines in ShiVa

First, we are moving our anonymous functions into true ShiVa functions. You can also optionally define parameters, like so for the second coroutine:

  1. --------------------------------------------------------------------------------
  2. function loader.co2 ( nIn )
  3. --------------------------------------------------------------------------------
  4.  
  5. local i = 0
  6. while i < 200 do
  7. log.message("f.co2_"..i .." & input: " ..nIn)
  8. --log.message("f.co2_"..i)
  9. i = i + 1
  10. coroutine.yield()
  11. end
  12.  
  13. this.done_co2 ( true )
  14.  
  15. --------------------------------------------------------------------------------
  16. end
  17. --------------------------------------------------------------------------------

It contains mostly the same code as before, but the function now uses a standard ShiVa layout and will be listed in an AIModel. Shortly before the coroutine finishes, it switches the this.done_co2() flag to TRUE, so our main thread can test against this control flag in order to know when the function has finished.

The coroutines themselves will be run in a ShiVa AIModel state, to keep everything compact and simple. in onEnter, we will reset the notification flags and define our functions as coroutines in a global context. Remember to keep the global names unique in order to avoid conflicts.

  1. --------------------------------------------------------------------------------
  2. function loader.coheavy_onEnter ( )
  3. --------------------------------------------------------------------------------
  4.  
  5. -- reset coroutine control variables
  6. this.done_co1 ( false )
  7. this.done_co2 ( false )
  8.  
  9. -- define functions as coroutines
  10. t_co1 = coroutine.create( this.co1 )
  11. t_co2 = coroutine.create( this.co2 )
  12.  
  13. --------------------------------------------------------------------------------
  14. end
  15. --------------------------------------------------------------------------------

The magic happens in onEnterFrame(). As this part of the state is run once per frame, our coroutines will yield after every single iteration and give the ShiVa engine time to do other things, like updating the picture, input, physics, and so forth. In other words, we are spreading out a loop of 200 iterations onto 200 frames, instead of dumping it all into a single one. Note the absence of a for-loop:

  1. --------------------------------------------------------------------------------
  2. function loader.coheavy_onLoop ( )
  3. --------------------------------------------------------------------------------
  4.  
  5. coroutine.resume(t_co1)
  6. coroutine.resume(t_co2)
  7.  
  8. -- return to idle if coroutines are finished
  9. if ( this.done_co1 ( ) and this.done_co2 ( ) ) then
  10. this.idle ( )
  11. end
  12.  
  13. --------------------------------------------------------------------------------
  14. end
  15. --------------------------------------------------------------------------------

As a means to return to an idle() state, we test against the two this.done*() flags and exit to another state when both are true.

The function loader.co2 has an additional function parameter, which we can address through coroutine.resume():

coroutines

This makes it possible to transmit data to the very next iteration of t_co2, possibly sending data that was just created in t_co1, creating an interlocking chain or threads without the locking problems commonly found in true multithreading languages.

The possible applications of coroutines include:
- loading large arrays of objects in the background while a scene in the foreground behaves normally / can be animated
- animated loading screens / loading mini games
- pseudo "streaming" content from a distant server one object at a time, instead of pausing until everything is loaded
- cleanly coding interdependent functions that rely on each others' data
- taking functions with long wait cycles off the CPU, avoiding writing busy loops
- and many more

Coroutines are an interesting and rather complex topic, which is impossible to cover in a blog tutorial like this. We highly recommend looking at online resources like
- Lua Doc: coroutine basics,
- Lua Doc: coroutine iterators,
- Lua Doc: coroutine API, and
- StackOverflow in order to get a better handle on them.