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 to Python. Growing frustrated with the Snake for different reasons, I wanted a small and simple-to-use language. One of my criteria was that I could embed it into a project's 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 that was neglected far too often with script languages.
Lua ticked many boxes for me and grew on to become a favorite 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 give an overview of how and with what I personally use Lua; this is not an 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 may 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 user's 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 (that 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 the 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 then be executed via Lua, printing the result on 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, run 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, creates a comfortable environment for 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 an "extensible extension language", referring to it being created with the idea of having a scripting language that can extend and customize 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 a dependency by creating a new
vendor/sol2/CMakeLists.txt
file, which is 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 for getting Lua and Sol2 into the project. Next up is 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,
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 then 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 in the official Sol2 documentation.
What comes next
This just showed some basics of Lua and parts of how I use Lua personally, only scratching the surface. To extend 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 👋🏻