Goodbye snake, I go to the moon

Lua in the sky
NASA is not the only one going to the moon — Lua, (pronounced LOO-ah) meaning "Moon" in Portuguese, is a multiple-paradigm scripting language. Created in 1993 by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes, members of the Computer Graphics Technology Group (Tecgraf) at the Pontifical Catholic University of Rio de Janeiro in Brazil.
Initially overlooked by me as "just another scripting language", it caught my eye when I was looking for an alternative for Python. Having growing frustrations with the Snake for different reasons, I wanted a small and simple to use language. One of my criteria was that I can embed it into a projects build system; making setup and management easier, sparing the need for specific version installations, or handling local installed language environments with one of too many tools. Speed was another factor, neglected far too often with script languages.
Lua ticked many boxes for me, and grew on to become a favourite of mine, from small scripts to large tools; a language as a general toolset for my daily tasks, running really everywhere. In this article I will show an overview of how and with what I personally use Lua, no exhaustive guide. Nevertheless, I will publish a multipart series on Lua going in-depth on many parts of the language; more on this at the end.
Quick setup
You maybe already have Lua installed on your system! You can check
by running
lua -v
in a terminal.
$ lua -v
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
$
is used to show a command will be entered.
If this is not the case there are different ways to get Lua, one way on macOS is using brew.
$ brew install lua
Having Lua installed you can start a
REPL
with the lua
command.
$ lua
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
> print("Hello, reader!")
Hello, reader!
>
signals the REPL prompt.
Add packages
LuaRocks can be seen as the de facto standard package manager for Lua. If not already installed, on macOS you can use brew to install LuaRocks.
$ brew install luarocks
Similar to NPM, I want to keep packages
local and inside the project, rather than global or
in my user directory. A global installation is the default, using
--local
will install packages in a folder in the users
home directory.
I will install all packages inside a lua_modules
folder
in the root of a project, using the --tree
option of
the LuaRocks CLI. As an
example, installing the
inspect
package.
$ luarocks install --tree=lua_modules inspect
For Lua to find any local package on require, the search paths need
to be set. This can be done by defining a setup module (can have any
name), that will be loaded when executing a script through the
-l
option of the Lua
CLI.
-- setup.lua
local version = _VERSION:match("%d+%.%d+")
package.path = 'lua_modules/share/lua/'
.. version ..
'/?.lua;lua_modules/share/lua/'
.. version ..
'/?/init.lua;'
.. package.path
package.cpath = 'lua_modules/lib/lua/'
.. version ..
'/?.so;' .. package.cpath
Creating a script file …
-- script.lua
local inspect = require "inspect"
local a = {1, 2}
print(inspect(a))
… and running it with the -l
option will find
the inspect
package from the local folder and execute
the script.
$ lua -l setup script.lua
{ 1, 2 }
This will now work for any package installed inside the local
project via
luarocks install --tree=lua_modules <package>
.
Environment setup
Setting up a project would not be complete without testing, lint tooling and code formatting.
Unit testing
For unit tests I use LuaUnit, installed via LuaRocks.
$ luarocks install luaunit --tree=lua_modules
Tests are just Lua files importing LuaUnit. I define test files with
the
.test.lua
extension and use
TAP (Test Anything Protocol)
as output format, an amazing output format I also use in
other languages.
-- src/some.test.lua
local lu = require('luaunit')
-- luacheck: globals TestCompare
TestCompare = {}
function TestCompare.test1()
local A = {1, 2}
local B = {1, 2}
lu.assertEquals(A, B)
end
function TestCompare.test2()
local A = {"a", "b"}
local B = {"a", "b"}
lu.assertEquals(A, B)
end
os.exit(lu.LuaUnit.run())
The test can than be executed via Lua, printing the result in the terminal.
$ lua -l setup src/some.test.lua -o tap
1..2
# Started on Mon Jan 01 10:11:42 2022
# Starting class: TestCompare
ok 1 TestCompare.test1
ok 2 TestCompare.test2
# Ran 2 tests in 0.000 seconds, 2 successes, 0 failures
Lint tooling
To lint I use LuaCheck, again installed via LuaRocks.
$ luarocks install luacheck --tree=lua_modules
Using the default options, running it on a whole folder, for example
the
src
folder.
$ ./lua_modules/bin/luacheck src
Checking src/main.lua OK
Checking src/some.test.lua OK
Total: 0 warnings / 0 errors in 2 files
Code formatting
Auto formatting Lua code happens via LuaFormatter.
$ luarocks install \
--server=https://luarocks.org/dev luaformatter \
--tree=lua_modules
For the checker I will use a small set of custom options, defined in
a
.lua-format
file (the format is YAML), in
the project root.
# .lua-format
use_tab: true
indent_width: 1
continuation_indent_width: 1
tab_width: 4
Running it on all files in a folder and formatting them.
$ ./lua_modules/bin/lua-format --in-place src/**/*.lua
Editor
Using Visual Studio Code, with a plugin for LuaCheck and LuaFormatter, creating a comfortable environment working with Lua.

C++, CMake, and Lua
Having established a base working with Lua, how about embedding it into other software? Lua is also known as "extensible extension language", referring to it being created with the idea to have a scripting language that can extend and customise applications.
I will use
Sol2, a header only C++ library providing Lua bindings in combination
with my
C++ base template
I created in my
basic C++ setup with CMake article. Having that set up, I first need to add Lua to the project;
creating a new CMake file vendor/lua/CMakeLists.txt
to
build Lua as it does not come with CMake support.
# vendor/lua/CMakeLists.txt
message(STATUS "Fetching Lua ...")
project(Lua LANGUAGES C)
set(CMAKE_C_STANDARD 17)
FetchContent_GetProperties(lua)
if (NOT lua_POPULATED)
FetchContent_Populate(lua)
endif ()
add_library(lua
${lua_SOURCE_DIR}/lapi.c ${lua_SOURCE_DIR}/lcode.c
${lua_SOURCE_DIR}/lctype.c ${lua_SOURCE_DIR}/ldebug.c
${lua_SOURCE_DIR}/ldo.c ${lua_SOURCE_DIR}/ldump.c
${lua_SOURCE_DIR}/lfunc.c ${lua_SOURCE_DIR}/lgc.c
${lua_SOURCE_DIR}/llex.c ${lua_SOURCE_DIR}/lmem.c
${lua_SOURCE_DIR}/lobject.c ${lua_SOURCE_DIR}/lopcodes.c
${lua_SOURCE_DIR}/lparser.c ${lua_SOURCE_DIR}/lstate.c
${lua_SOURCE_DIR}/lstring.c ${lua_SOURCE_DIR}/ltable.c
${lua_SOURCE_DIR}/ltm.c ${lua_SOURCE_DIR}/lundump.c
${lua_SOURCE_DIR}/lvm.c ${lua_SOURCE_DIR}/lzio.c
${lua_SOURCE_DIR}/lauxlib.c ${lua_SOURCE_DIR}/lbaselib.c
${lua_SOURCE_DIR}/lcorolib.c ${lua_SOURCE_DIR}/ldblib.c
${lua_SOURCE_DIR}/liolib.c ${lua_SOURCE_DIR}/lmathlib.c
${lua_SOURCE_DIR}/loadlib.c ${lua_SOURCE_DIR}/loslib.c
${lua_SOURCE_DIR}/lstrlib.c ${lua_SOURCE_DIR}/ltablib.c
${lua_SOURCE_DIR}/lutf8lib.c ${lua_SOURCE_DIR}/linit.c)
target_include_directories(lua PUBLIC ${lua_SOURCE_DIR})
FetchContent_MakeAvailable(lua)
This will set up Lua as a C project and create a library under the
name
lua
. Next, extending the dependency list in
vendor/CMakeLists.txt
.
# vendor/CMakeLists.txt
FetchContent_Declare(
lua
GIT_REPOSITORY "https://github.com/lua/lua.git"
GIT_TAG v5.4.4
)
add_subdirectory(lua)
Sol2 can be added as dependency by creating a new
vendor/sol2/CMakeLists.txt
file, rather short as it comes with CMake support.
# vendor/sol2/CMakeLists.txt
message(STATUS "Fetching Sol2 ...")
FetchContent_MakeAvailable(sol2)
Again, adding it to the dependency list in
vendor/CMakeLists.txt
.
# vendor/CMakeLists.txt
# ...
FetchContent_Declare(
sol2
GIT_REPOSITORY "https://github.com/ThePhD/sol2.git"
GIT_TAG v3.3.0
)
add_subdirectory(sol2)
That's it getting Lua and Sol2 into the project. Next up, adding the libraries to the application by linking them to the target.
# src/app/CMakeLists.txt
set(NAME "App")
include(${PROJECT_SOURCE_DIR}/cmake/StaticAnalyzers.cmake)
add_executable(${NAME} App/Main.cpp)
target_include_directories(${NAME}
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(${NAME}
PRIVATE cxx_std_20)
# Link lua and sol2 to the target here.
target_link_libraries(${NAME}
PRIVATE project_warnings Core lua sol2)
Now that everything is ready, trying a small test program in
src/app/App/Main.cpp
.
// src/app/App/Main.cpp
#include <sol/sol.hpp>
#include "Core/Log.hpp"
int main() {
sol::state lua{};
int some_val{0};
lua.set_function("beep", [&some_val] {
++some_val;
});
lua.script("beep()");
APP_INFO("Some value is now: {}", some_val);
return 0;
}
This will indeed print Some value is now: 1
. A
different example, using a Lua script file with a
simple function for demonstration.
-- src/app/App/hello.lua
function hello(name)
return "Hello, " .. name
end
With CMake, any non build files need to be copied to the build
output directory, using the file
function this can be
done on the CMake configure step.
# src/app/CMakeLists.txt
set(NAME "App")
# Copy the Lua script to the build destination.
# This could be multiple files or a whole folder.
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/App/hello.lua
DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
# More CMake ...
The script file can than be loaded, extracting the defined Lua
function
hello
to call it from C++.
// src/app/App/Main.cpp
#include <sol/sol.hpp>
#include "Core/Log.hpp"
int main() {
sol::state lua{};
// Load the script file into the state.
lua.script_file("hello.lua");
// Extract the function.
sol::function hello_fn{lua["hello"]};
std::function<std::string(std::string)> hello{hello_fn};
// Call and print the result.
APP_INFO("Result: {}", hello("Mr. Anderson"));
return 0;
}
Running this will print Result: Hello, Mr. Anderson
.
This example project is also available on GitHub as embedding-lua-in-cpp-example repository. How to inject new functions and values into Lua, as well as many other things, can be found at the official Sol2 documentation.
What comes next
This just showed some basics using Lua, and parts of how I use Lua personally, only scratching the surface. To extend on this and shine an even brighter light on the Moon, I will create a series of articles covering many topics of Lua more in-depth. Some of those will be:
- In-depth Project setup using LuaRocks
- All about dependencies and creating your own modules
- Testing
- Debugging, and profiling
- CLI applications with Lua
- Scaling Lua applications and large Lua projects
- Building and extending C++ applications with embedded Lua
And maybe more, if I find more things to cover. Until then 👋🏻