Martin Helmut Fieber

Goodbye snake, I go to the moon

Posted on — Updated

Showing the Visual Studio Code editor with an open Lua test file in the middle and the project setup with different files on the left.
Editor setup for Lua with Visual Studio Code.

Lua in the sky

NASA is not the only one going to the moonLua (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
The $ 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!
The > 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.

A view of the Code editor with an open Lua test file.
Example editor setup with Visual Studio Code.

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 👋🏻

← Show all blog posts