Modularizing Lua - when to use, why and how?

Many Storyboard applications evolve and accumulate Lua functions as the application fills-out.  These utility Lua functions are typically clustered in a few .lua files under the root of the scripts folder of the Storyboard project file structure which works fine whilst the number of screens is limited however as this grows or the project team increases then the task of collaborating and managing the functionality can start to become more challenging.

By default all of the .lua files in the script folder root are loaded and parsed by the Lua plugin early on during application initialisation and global functions are executed, data variables allocated etc such that everything is ready for the application to display the initial screen. This is perfectly acceptable but perhaps there are some better ways to structure the application.

Taking the time to review the application structure and potentially breaking up the Lua functionality to sub-modules can help with maintenance and development flow plus it can have other benefits that may not be quite so obvious.

The benefits of modularizing your Lua

One common approach is to localise the Lua functions specific to each screen in a separate file and perhaps organise these into sub-folders within the project whilst keeping common re-usable utility functions at the top of the tree directly under the scripts folder:

image2.png

When you break-up the Lua code for the project in to modules by creating directories under the scripts directory as shown above this will change the way that the Lua interpreter handles the contents:

  1. It will not automatically load those scripts not located in the root of the scripts until a require operand is issued for the script file

  2. Functionality and local data variables specific to a given screen that are only needed ‘just-in-time’ for that screen and can often be disposed of once you leave which helps to manage memory utilisation

  3. Delay loading will allow certain modules to be loaded at specific times throughout the execution of the UI and for example can help you optimise the startup time and external dependencies

  4. Larger project teams can work on developing individual screens without impacting the rest of the application by localising their code

  5. It helps when you need to write unit tests for individual screens by localising Lua scripting functionality

Lua Modules 101

Lua modules can come in a number of different flavours from simple global function groupings to full object-oriented class implementations.

It first worth an overview of what the require() does in Lua and how to use it to control how Storyboard loads and parses all of the Lua files.  Take a look at the details here (Note: the Storyboard Lua interpreter is currently based on standard Lua 5.1):

https://www.lua.org/manual/5.1/manual.html#5.3

http://lua-users.org/wiki/ModulesTutorial

The default case is that the runtime loads all of Lua the files located in the root of the scripts folder on startup. The links above explain the way that the Lua module loader scans for modules referenced in a require statement and the role of the package.cpath in directing Lua which path to search.

Tip

This is a common area where you may find issues with integrating 3rd party Lua libraries and so is worth reading and understanding.

Within the Storyboard application you can exploit this capability to explicitly control the loading and execution of any additional Lua sub modules by inserting the appropriate require statements to pull-in module dependencies.

Re-factoring the Lua functionality

As outlined in the previous sections there are some of the benefits in taking the additional steps described to restructure the application and Lua components. It hopefully delivers a better software architecture and project organisation that is recommended as best practice by our own apps developers.

For example if there was a module for each specific screen that was in the UI, the following design pattern could be used to delay load and unload the Lua module (and any dependencies) on demand.

First you might use a screenshow.pre event trigger on that screen to load the Lua module using a Lua function call:

Locan main_screen = nil
function screenshow_pre()
  main_screen = require("modules.mainScreen")
end

Then on the screenhide.post event, run the following code to unload the Lua module:

function screenhide_post()
  main_screen = nil
  package.loaded["modules.mainScreen"] = nil
  collectgarbage("collect") --if you aren't using the gc option at engine level
end

The require is undone by the line main_screen = nil and the package.loaded[“screens/main_screen”] = nil will remove the code from the internal Lua table and enable that memory to be recycled if needed.

The runtime will do this recycling automatically and for most platforms this is transparent. However to force Lua to be more aggressive you can set the gc=1 option to the Lua plugin.  This will cause the Lua interpreter to perform a garbage collection cycle after each Lua action that is performed.  This can help with fragmentation as objects that were just allocated and discarded will be freed immediately rather than left to linger for an undetermined amount of time.

For performance however, it may be better to not use the (global) gc=1 option and instead clean-up when leaving the specific screen. This can be achieved programmatically using a call to collectgarbage("collect") at appropriate points in your application.

Note

In our example this may slow the screen transition slightly, but should not hinder the execution once on the particular screen or impact other behaviours elsewhere in the system.

Improving application structure with template Lua module code

The simplistic approach outlined above will suffice for applications with a few screens however you will see that there are some improvements that can be made once you factor in other common application requirements and look to make the code more reusable.

This is just a simple example and used by way of illustration rather than a reference standard for developing with Lua in Storyboard!

For a better structure to our application code that provides benefit for developing even simple Storyboard applications and our in-house developers recommend adopting some object oriented design practices when coding with Lua.

It is generally the case that each screen would have a common requirement for some initial logic and variable setup to perform ‘On Entry’ before the screen is shown plus there may be some clean-up to do such as disposing of data objects and tables or clearing down animation and timers ‘On Exit’.

The general idea is that each screen has a Lua module file of the same name (watch-out for case sensitivity!) under the ‘modules’ sub-folder and that each file contains as a minimum a couple of template functions for the init and tear-down specific to that screen.

Obviously this base Lua content can be used as a code template and copied for each screen in turn and become a basis for defining the application development guidelines for the project.

Lua makes heavy use of ‘tables’ at the core of the language, indexed by strings that are used for look-up and manipulation using the extensive string libraries. When we re-factor the Lua code into modules we can use the string library to access the table of functions contained in our module by updating the metatable for strings, where the __index field points to the string table, and adding to it our class namespace.

In practice within our example application as shown we will have only a single instance of the class created implicitly (a singleton) and we are not making use of the new member function. Hence it is not being invoked however it is useful good practice to add this to our module implementations.

So for example in our Lua 5.1 syntax employing object-oriented notation:

https://www.lua.org/manual/5.1/manual.html#5.4

modules/mainScreen.lua

---
-- @module mainScreen
--  This class contains functions to underpin the mainScreen functionality. 
mainScreen = {}
mainScreen.__index = mainScreen

print("loaded module mainScreen.lua")

-- Create a new module instance and update metatable 
function mainScreen.new(subsystem)
local self = setmetatable({}, mainScreen)
self:init(subsystem)
return self
end 

-- Perform any dynamic screen specific configuration here
function mainScreen:init(mapargs) 
print("mainScreen:init()")
end

-- Perform any dynamic screen specific tear-down here
function mainScreen:exit(mapargs) 
print("mainScreen:exit()" )
end

-- Callback for 'Press' button to be triggered by gre.press action
function mainScreen:CBHandlePress(mapargs) 
local data={}
data["mainLayer.status_text.text"] = "Activated"
gre.set_data(data)
end

-- Callback for 'Press' button to be triggered by gre.release and gre.outbound actions
--- @param gre#context mapargs
function mainScreen:CBHandleRelease(mapargs) 
local data={}
data["mainLayer.status_text.text"] = "Deactivated"
gre.set_data(data)
end

And for a second screen which in this example performs some data and screen initialisation by assigning a fairly large text string and then explicitly clearing this on exit so that the Lua garbage collection can recycle this memory once finished with.

modules/secondScreen.lua

---
-- @module secondScreen
--  This class contains functions to underpin the secondScreen funtionality. 
secondScreen = {}
secondScreen.__index = secondScreen

local screen_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

print("loaded module secondScreen.lua")

-- Create a new module instance and update metatable 
function secondScreen.new(subsystem)
local self = setmetatable({}, secondScreen)
self:init(subsystem)
return self
end 

-- Perform any dynamic screen configuration here
function secondScreen:init(mapargs) 
print("secondScreen:init()") 
local data = {}
data["navLayer.content_text.text"] = screen_text
gre.set_data(data)
end

-- Perform any dynamic screen specific tear-down here
function secondScreen:exit(mapargs) 
print("secondScreen:exit()")
screen_text = nil
end

Because these functions are now localised to a specific module namespace for each screen we must refer to them in Storyboard using their full path using module class notation eg : mainScreen::CBHandlePress and mainScreen::CBHandleRelease  as below:

image3.png

Putting it all together from the top down

By exploiting the context metadata for the events passed via the mapargs parameter we can create some generic utility functions for the application to use when triggered by loading (screenshow.pre) and unloading (screenhide.pre) screen events on each screen.  As this is common (global namespace) utility code it can be located at the scripts root folder in the file called callbacks.lua for this example, so that it is always loaded and present by default.

The require statement coupled with the module definition and metatable entry in each module file now means that when the module namespace is loaded it is added to the global table so we can now access the functions specific to each module via their class.

We can load the module via lookup of the module path (here we are using screen name as the name of the Lua module) using the event context for convenience. Note the use of a ‘.’ (dot) separator in the module name to instruct the Lua loader path:

-- Load sub module on demand
-- NOTE: this context strategy requires that the lua module for the screen
-- is named the same as the screen
local module_name = "modules." .. mapargs.context_screen 
require( module_name )

local module = _G[mapargs.context_screen]
module.CBMyFunc()

Pulling this together into the top-level callbacks.lua file completes our implementation.

callbacks.lua

---
-- @module callbacks
--  This class contains functions to underpin the general callbacks funtionality. 
--  It is intended to be an application global utility class
--  
callbacks = {}
callbacks.__index = callbacks

print("loaded callbacks.lua")
-- Load sub module on start-up
--require("modules/mainScreen")
--require("modules/secondScreen")
--print("Lua memory used:"..collectgarbage("count").."KB")

-- Create a new module instance and update metatable 
function callbacks.new(subsystem)
  local self = setmetatable({}, callbacks)
  self:init(subsystem)
  return self
end

-- Perform any dynamic module specific configuration here
function callbacks:init(mapargs) 
  print("callbacks:init()")
end

--- @param gre#context mapargs
function callbacks:CBHandleScreenShow(mapargs) 
  -- Load sub module on demand
  -- NOTE: this context strategy requires that the lua module for the screen
  -- is named the same as the screen
  local module_name = "modules." .. mapargs.context_screen 
  require( module_name )
  
  -- Invoke loaded screen module specific initialisation function
  local module = _G[mapargs.context_screen]
  module:init(mapargs)
  --print("Lua memory used:"..collectgarbage("count").."KB")
end

--- @param gre#context mapargs
function callbacks:CBHandleScreenHide(mapargs) 
  -- Unload sub module on exit
  -- NOTE: this context strategy requires that the lua module for the screen
  -- is named the same as the screen
  local module_name = "modules." .. mapargs.context_screen 
  local module = _G[mapargs.context_screen]
  print("unloading module : " .. module_name )
  
  -- Invoke loaded screen module specific tear-down function
  module:exit()
  
  -- Release and clean-up sub module on demand
  package.loaded[ module_name ] = nil
  collectgarbage("collect") --if you aren't using the gc option at engine level
  --print("Lua memory used:"..collectgarbage("count").."KB")
end

The Lua function call collectgarbage("count") can be used to query the memory retained by the Lua runtime for debug and monitoring purposes.

In Storyboard 6.2 there is now an ---ADVANCED--- option for the Function Name within the Lua callback action properties dialog which will enables you to drill down into to select functions within classes.

Note at present this dialog will only show the classes defined at the application global level.

image1.png

Observing the new modules structure in action

Now we can observe the start-up and early initialisation of our top-level callbacks.lua functions then the subsequent delay loading of the screen specific Lua modules ‘just-in-time’ for their associated screen. The utility functions manage their lifespan automatically on screenshow.pre and screenhide.pre.

The trace output snippet below launched in the Designer simulation environment shows the the behaviour of our application modules and functions as follows:

sbengine -vvvvvv -ogreio,channel=LuaModulesTest LuaModuleTest-480x272.gapp

INFO [0.070]:Initialize plugin [greio (6.2.0.36502)] [channel=LuaModulesTest]
INFO [0.070]:Initialize plugin [harfbuzz (6.2.0.36502)] []
INFO [0.070]:Initialize plugin [lua (6.2.0.36502)] []
ACTION [0.071]:Loading Lua script [callbacks.lua]
loaded callbacks.lua
INFO [0.071]:Initialize plugin [luagredom (6.2.0.36502)] []
INFO [0.071]:Initialize plugin [media (6.2.0.36502)] []
INFO [0.071]:Initialize plugin [metrics (6.2.0.36502)] []
INFO [0.072]:Initialize plugin [poly (6.2.0.36502)] []
INFO [0.072]:Initialize plugin [rext-external (6.2.0.120)] []
INFO [0.072]:Initialize plugin [rtext (6.2.0.36502)] []
INFO [0.072]:Initialize plugin [screen-dump (6.2.0.36502)] []
INFO [0.072]:Initialize plugin [screen-fade (6.2.0.36502)] []
INFO [0.072]:Initialize plugin [screen-path (6.2.0.36502)] []
INFO [0.072]:Initialize plugin [screen-rotate (6.2.0.36502)] []
INFO [0.072]:Initialize plugin [screen-scale (6.2.0.36502)] []
INFO [0.073]:Initialize plugin [system (6.2.0.36502)] []
INFO [0.073]:Initialize plugin [timer (6.2.0.36502)] []
EVENT [0.074]:IO: Dispatch [gre.internalinit]
EVENT [0.108]:IO: Queue [1] gre.init INFO [0.109]:gesture: not enabled, no multi-touch events used in application
EVENT [0.115]:SBIO: Receiver registered @ [LuaModulesTest]
EVENT [0.116]:IO: Dispatch [gre.init]
EVENT [0.116]:IO: Queue [2] gre.screenshow.pre DIAG [0.116]:CONTROL RENDER: background (0x0 479x271)
DIAG [0.116]:CONTROL RENDER: status_text (277x62 476x164)
DIAG [0.118]:CONTROL RENDER: button_next (395x206 469x261)
DIAG [0.119]:CONTROL RENDER: button_test (203x86 277x141)
DIAG [0.120]:CONTROL RENDER: header_text (111x7 350x26)
EVENT [0.120]:IO: Queue [3] gre.screenshow.post EVENT [0.121]:IO: Dispatch [gre.screenshow.pre]
ACTION [0.121]:ACTION: Invoke [gre.screenshow.pre]->[gra.lua] on screen [mainScreen]
ACTION [0.121]:Lua: [callbacks.CBHandleScreenShow]
loaded module mainScreen.lua
mainScreen:init()
EVENT [0.122]:IO: Dispatch [gre.screenshow.post]
ACTION [1.661]:ACTION: Invoke [gre.outbound]->[gra.datachange] on control [button_test]
ACTION [1.661]:ACTION: Invoke [gre.outbound]->[gra.lua] on control [button_test]
ACTION [1.661]:Lua: [mainScreen::CBHandleRelease]
DIAG [1.661]:CONTROL RENDER: background (277x62 476x164)
DIAG [1.662]:CONTROL RENDER: status_text (277x62 476x164)
DIAG [1.662]:CONTROL RENDER: button_test (277x86 277x141)
EVENT [2.200]:IO: Queue [1] gre.press EVENT [2.200]:IO: Dispatch [gre.press]
ACTION [2.200]:ACTION: Invoke [gre.press]->[gra.datachange] on control [button_test]
ACTION [2.201]:ACTION: Invoke [gre.press]->[gra.lua] on control [button_test]
ACTION [2.201]:Lua: [mainScreen::CBHandlePress]
DIAG [2.201]:CONTROL RENDER: background (203x62 476x164)
DIAG [2.201]:CONTROL RENDER: status_text (277x62 476x164)
DIAG [2.201]:CONTROL RENDER: button_test (203x86 277x141)
EVENT [2.404]:IO: Queue [1] gre.release EVENT [2.404]:IO: Dispatch [gre.release]
ACTION [2.405]:ACTION: Invoke [gre.release]->[gra.datachange] on control [button_test]
ACTION [2.405]:ACTION: Invoke [gre.release]->[gra.lua] on control [button_test]
ACTION [2.405]:Lua: [mainScreen::CBHandleRelease]
DIAG [2.405]:CONTROL RENDER: background (203x62 476x164)
DIAG [2.405]:CONTROL RENDER: status_text (277x62 476x164)
DIAG [2.405]:CONTROL RENDER: button_test (203x86 277x141)
ACTION [3.203]:ACTION: Invoke [gre.outbound]->[gra.datachange] on control [button_test]
ACTION [3.203]:ACTION: Invoke [gre.outbound]->[gra.lua] on control [button_test]
ACTION [3.203]:Lua: [mainScreen::CBHandleRelease]
ACTION [3.373]:ACTION: Invoke [gre.outbound]->[gra.datachange] on control [button_next]
EVENT [3.699]:IO: Queue [1] gre.press EVENT [3.699]:IO: Dispatch [gre.press]
ACTION [3.699]:ACTION: Invoke [gre.press]->[gra.datachange] on control [button_next]
DIAG [3.700]:CONTROL RENDER: background (395x206 469x261)
DIAG [3.700]:CONTROL RENDER: button_next (395x206 469x261)
EVENT [3.896]:IO: Queue [1] gre.release EVENT [3.896]:IO: Dispatch [gre.release]
ACTION [3.896]:ACTION: Invoke [gre.touch]->[gra.screen] on control [button_next]
EVENT [3.897]:IO: Queue [2] gre.screenshow.pre EVENT [3.897]:IO: Queue [3] gre.screenhide.pre ACTION [3.897]:ACTION: Invoke [gre.release]->[gra.datachange] on control [button_next]
EVENT [3.897]:IO: Dispatch [gre.screenshow.pre]
ACTION [3.897]:ACTION: Invoke [gre.screenshow.pre]->[gra.lua] on screen [secondScreen]
ACTION [3.897]:Lua: [callbacks.CBHandleScreenShow]
loaded module secondScreen.lua
secondScreen:init()
EVENT [3.898]:IO: Dispatch [gre.screenhide.pre]
ACTION [3.898]:ACTION: Invoke [gre.screenhide.pre]->[gra.lua] on screen [mainScreen]
ACTION [3.898]:Lua: [callbacks.CBHandleScreenHide]
unloading module : modules.mainScreen
mainScreen:exit()
:
ACTION [4.915]:ACTION: Invoke [gre.touch]->[gra.screen] on control [button_back]
EVENT [4.915]:IO: Queue [2] gre.screenshow.pre EVENT [4.915]:IO: Queue [3] gre.screenhide.pre ACTION [4.915]:ACTION: Invoke [gre.release]->[gra.datachange] on control [button_back]
EVENT [4.916]:IO: Dispatch [gre.screenshow.pre]
ACTION [4.916]:ACTION: Invoke [gre.screenshow.pre]->[gra.lua] on screen [mainScreen]
ACTION [4.916]:Lua: [callbacks.CBHandleScreenShow]
loaded module mainScreen.lua
mainScreen:init()
EVENT [4.916]:IO: Dispatch [gre.screenhide.pre]
ACTION [4.917]:ACTION: Invoke [gre.screenhide.pre]->[gra.lua] on screen [secondScreen]
ACTION [4.917]:Lua: [callbacks.CBHandleScreenHide]
unloading module : modules.secondScreen
secondScreen:exit()
DIAG [4.917]:CONTROL RENDER: background (0x0 479x271)
DIAG [4.917]:CONTROL RENDER: status_text (277x62 476x164)
DIAG [4.917]:CONTROL RENDER: button_next (395x206 469x261)
DIAG [4.917]:CONTROL RENDER: button_test (203x86 277x141)
DIAG [4.917]:CONTROL RENDER: header_text (111x7 350x26)
EVENT [4.918]:IO: Queue [2] gre.screenshow.post EVENT [4.918]:IO: Queue [3] gre.screenhide.post DIAG [4.918]:CONTROL RENDER: background (0x0 479x271)
DIAG [4.918]:CONTROL RENDER: status_text (277x62 476x164)
DIAG [4.918]:CONTROL RENDER: button_next (395x206 469x261)
DIAG [4.918]:CONTROL RENDER: button_test (203x86 277x141)
DIAG [4.919]:CONTROL RENDER: header_text (111x7 350x26)
EVENT [4.919]:IO: Dispatch [gre.screenshow.post]
EVENT [4.919]:IO: Dispatch [gre.screenhide.post]
ACTION [5.052]:ACTION: Invoke [gre.outbound]->[gra.datachange] on control [button_test]
ACTION [5.053]:ACTION: Invoke [gre.outbound]->[gra.lua] on control [button_test]
ACTION [5.053]:Lua: [mainScreen::CBHandleRelease]
EVENT [5.085]:IO: Queue [1] gre.release EVENT [5.085]:IO: Dispatch [gre.release]
EVENT [5.832]:IO: Queue [1] gre.quit EVENT [5.832]:IO: Dispatch [gre.quit]

LuaModulesTest.zip

Was this article helpful?
0 out of 0 found this helpful