andersch.dev

<2025-05-21 Wed>

Lua

Lua is a dynamically-typed, garbage collected programming language that can easily embedded as a scripting language in other programs.

Concepts

Basics

  • Variables are global by default. Use local to circumvent.
  • Optional semicolons
  • No curly braces {} for blocks
  • Comments: -- single line and --[[ multi-line --]]
  • Data Types: nil, true/false, number (double float), Integers (Lua 5.3+), string (immutable), function
  • Multiple return values: function foo() return 1, "two" end; local x, y = foo()
  • Variadic functions:

    function sum(...)
      local total = 0;
      for _, v in ipairs{...} do
        total = total + v
      end
      return total
    end
    
    print(sum(4,4))
    
    8
    
  • Error Handling: error("message"): Throws an error (like throw).
  • pcall(function, args...): "Protected call.", similar to try-catch.

    local status, result_or_error = pcall(function()
        -- some_risky_operation()
        return "Success!"
    end)
    if status then
        print("Succeeded:", result_or_error)
    else
        print("Failed:", result_or_error)
    end
    
    Succeeded:	Success!
    

Table Data Structure

Primary data structure is the table. Can represent arrays, dicts, and objects.

local mixed = {
  10,
  "hello",
  [5]   = "explicit index",
  key   = "value",
  foo   = "bar",
}

print(mixed[0])     -- nil because of 1-Based Indexing
print(mixed[5])     -- "explicit index"
print(#mixed)       -- length of the sequence part (2)
print(mixed["key"])
print(mixed.key)    -- syntactic sugar
mixed.thing = nil   -- deletes key from table
print(mixed["foo"])
mixed = {}          -- set to empty table
nil
explicit index
2
value
value
bar

Metatables and Metamethods

Every table can have a metatable. It contains special functions called metamethods. This is the mechanism used for operator overloading, inheritance, and customizing table behavior.

Metamethods include:

  • __index for prototypal inheritance:

    local defaults = { x = 0, y = 0 }
    local point = { x = 10 }
    setmetatable(point, { __index = defaults })
    print(point.x) -- 10 (from point itself)
    print(point.y) -- 0 (from defaults via __index)
    
    10
    0
    
  • __newindex: Called when trying to assign to a non-existent key in a table
  • __call: Allows table to be called like a function: myTable(arg1, arg2)
  • __add, __sub, __mul, __div, etc. for operator overloading.
  • __tostring: Called by print() or tostring()
  • setmetatable(table, metatable_or_nil), getmetatable(table)

OOP (Methods) in Lua

Vector = {} -- Our "class" (just a table)
Vector.__index = Vector -- Instances will look up methods in Vector itself

function Vector:new(x, y) -- Colon : is syntactic sugar for methods
    -- self:method(...) is like self.method(self, ...)
    local obj = {x = x or 0, y = y or 0}
    setmetatable(obj, Vector)
    return obj
end

function Vector:magnitude()
    return math.sqrt(self.x^2 + self.y^2)
end

local v1 = Vector:new(3, 4)
print(v1:magnitude()) -- 5
print(v1.x)           -- 3

Coroutines

Lua has cooperative coroutines (yield control explicitly). They are useful for iterators, generators, state machines, non-blocking I/O (in event loops).

local co = coroutine.create(function(start_val)
    print("Coroutine started with:", start_val)
    local val = start_val
    for i = 1, 3 do
        val = val + i
        print("Coroutine yielding:", val)
        coroutine.yield(val) -- Suspend and return val
    end
    print("Coroutine finished")
    return val + 100 -- Final return value
end)

print(coroutine.status(co)) -- suspended

local status, res
status, res = coroutine.resume(co, 10) -- Start it with 10
print("Main received:", status, res)   -- true, 11

status, res = coroutine.resume(co)
print("Main received:", status, res)   -- true, 13 (11+2)

status, res = coroutine.resume(co)
print("Main received:", status, res)   -- true, 16 (13+3)

status, res = coroutine.resume(co)
print("Main received:", status, res)   -- true, 116 (16+100)

print(coroutine.status(co)) -- dead (finished correctly)
suspended
Coroutine started with:	10
Coroutine yielding:	11
Main received:	true	11
Coroutine yielding:	13
Main received:	true	13
Coroutine yielding:	16
Main received:	true	16
Coroutine finished
Main received:	true	116
dead

Modules and require

  • require("modulename") loads modules
  • A module returns a table containing its public functions and data
  • Lua looks for modules in package.path and package.cpath (for C modules)
-- mymodule.lua
local M = {} -- Table for our module

function M.greet(name)
    return "Hello, " .. name .. "!"
end

M.version = "1.0"

return M -- Export the table

-- main.lua
local mymod = require("mymodule")
print(mymod.greet("Lua User")) -- Hello, Lua User!
print(mymod.version)           -- 1.0

Lua C API

The Lua C API for embedding Lua into C/C++ (or extending Lua with C functions) is stack-based. You push values onto a virtual stack, call Lua functions, and retrieve results from the stack.

#include <stdio.h>
#include <string.h>

// lua headers
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"

// ------------------- C function to be exposed to Lua -------------------
// Signature for C functions callable by Lua:
//     int function_name(lua_State *L);
// Returns number of return values pushed onto the stack
static int c_add(lua_State *L) {
    // 1. Check and get arguments from Lua stack
    if (lua_gettop(L) != 2) // returns number of items on the stack (also the index of the top element)
    {
        return luaL_error(L, "C: c_add expects 2 arguments"); // Throws a Lua error
    }
    if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2)) {
        return luaL_error(L, "C: c_add expects two numbers");
    }

    double num1 = lua_tonumber(L, 1); // Get first argument (index 1)
    double num2 = lua_tonumber(L, 2); // Get second argument (index 2)

    double sum = num1 + num2; // 2. Perform the operation
    lua_pushnumber(L, sum);   // 3. Push the result onto the Lua stack
    return 1;                 // 4. Return the number of results pushed
}

// ------------------- Error handler for lua_pcall -------------------
// This function is pushed onto the stack before calling lua_pcall.
// If an error occurs during lua_pcall, Lua calls this function with the error message on top of the stack.
static int msgh (lua_State *L) {
  const char *msg = lua_tostring(L, 1); // Get error message
  if (msg == NULL) {  // Does it have a metamethod that yields a string instead?
    if (luaL_callmeta(L, 1, "__tostring") &&  // Call __tostring metamethod
        lua_type(L, -1) == LUA_TSTRING)
      return 1;  // Return the result of __tostring
    else
      msg = lua_pushfstring(L, "(error object is a %s value)",
                               luaL_typename(L, 1));
  }
  luaL_traceback(L, L, msg, 1);  // Append a traceback
  return 1;  // Return the new error message (with traceback)
}


int main() {
    lua_State *L = luaL_newstate(); // Create a new Lua state
    if (L == NULL) {
        fprintf(stderr, "C: Error creating Lua state.\n");
        return 1;
    }

    luaL_openlibs(L); // Load standard Lua libraries (print, math, string, etc.)

    // --- Expose C function to Lua ---
    lua_pushcfunction(L, c_add);      // Push our C function onto the stack
    lua_setglobal(L, "c_add");        // Set it as a global Lua variable named "c_add"
    // Alternatively: lua_register(L, "c_add", c_add); // macro that does both

    printf("C: --- Running script.lua ---\n");
    // Load and run the Lua script file
    // We'll use lua_pcall for safer execution, so we need an error handler.
    // Push error handler function
    lua_pushcfunction(L, msgh);
    int msgh_idx = lua_gettop(L); // Get stack index of error handler

    if (luaL_loadfile(L, "script.lua") != LUA_OK) {
        // Error loading file (e.g., file not found, syntax error)
        fprintf(stderr, "C: Error loading script.lua: %s\n", lua_tostring(L, -1));
        lua_pop(L, 1); // Pop error message
        lua_close(L);
        return 1;
    }
    // luaL_loadfile pushes the compiled chunk onto the stack.
    // Now call it with lua_pcall(L, num_args, num_results, error_handler_index)
    // error_handler_index is the stack index of our error handler function.
    if (lua_pcall(L, 0, LUA_MULTRET, msgh_idx) != LUA_OK) {
        fprintf(stderr, "C: Error running script.lua: %s\n", lua_tostring(L, -1));
        // Error message is on top of the stack, error handler (msgh) already processed it.
        lua_pop(L, 1); // Pop error message
        lua_close(L);
        return 1;
    }
    lua_remove(L, msgh_idx); // Remove error handler from stack

    printf("C: --- Script finished. Accessing Lua global variable ---\n");
    lua_getglobal(L, "lua_message"); // Pushes the value of global 'lua_message' onto the stack
    if (lua_isstring(L, -1)) {     // -1 refers to the top of the stack
        const char *msg_from_lua = lua_tostring(L, -1);
        printf("C: Value of 'lua_message': %s\n", msg_from_lua);
    } else {
        printf("C: 'lua_message' is not a string or not found.\n");
    }
    lua_pop(L, 1); // Pop the value from the stack

    printf("C: --- Calling Lua function 'lua_multiply' from C ---\n");
    lua_getglobal(L, "lua_multiply"); // Get the function
    if (!lua_isfunction(L, -1)) {
        fprintf(stderr, "C: Error: 'lua_multiply' is not a function in Lua.\n");
        lua_pop(L, 1); // Pop non-function
    } else {
        // Push error handler again for this pcall
        lua_pushcfunction(L, msgh);
        lua_insert(L, -2); // Move msgh to just before the function on stack
        msgh_idx = lua_gettop(L) - 1;

        lua_pushnumber(L, 7);  // Push first argument
        lua_pushnumber(L, 6);  // Push second argument

        // Call: lua_pcall(L, num_args, num_results, error_handler_index)
        if (lua_pcall(L, 2, 1, msgh_idx) != LUA_OK) {
            fprintf(stderr, "C: Error calling 'lua_multiply': %s\n", lua_tostring(L, -1));
            lua_pop(L, 1); // Pop error
        } else {
            if (lua_isnumber(L, -1)) {
                double product = lua_tonumber(L, -1);
                printf("C: lua_multiply(7, 6) returned: %f\n", product);
            } else {
                printf("C: 'lua_multiply' did not return a number.\n");
            }
            lua_pop(L, 1); // Pop the result
        }
        lua_remove(L, msgh_idx); // Remove error handler
    }

    printf("C: --- Testing Lua error handling from C ---\n");
    lua_getglobal(L, "lua_error_func");
    if (lua_isfunction(L, -1)) {
        lua_pushcfunction(L, msgh);
        lua_insert(L, -2);
        msgh_idx = lua_gettop(L) - 1;

        if (lua_pcall(L, 0, 0, msgh_idx) != LUA_OK) { // 0 arguments, 0 results
            fprintf(stderr, "C: Caught error from 'lua_error_func': %s\n", lua_tostring(L, -1));
            lua_pop(L, 1); // Pop the error message
        }
        lua_remove(L, msgh_idx);
    } else {
        lua_pop(L,1); // pop non-function
    }

    lua_close(L); // close Lua state, free resources
    printf("C: Lua state closed.\n");
    return 0;
}

Dialects

There are Lua dialects and variations that can differ in their implementation (e.g., JIT compiler vs. standard interpreter) and language features.

LuaJIT

  • Based on Lua 5.1 + extensions
  • Features a very fast interpreter and a JIT compiler that can speed up Lua code
  • FFI (Foreign Function Interface) for calling C functions directly from Lua

Luau (Lune Runtime)

  • Gradually typed superset of Lua developed by Roblox.
  • Has its own VM, interpreter, and compiler.
  • Features sandboxing & syntax additions

Nelua

  • Statically typed, compiled-to-C, manually memory managed language
  • Mostly it's own language with "Lua flavor"
  • Can interoperate with Lua and C easily
  • Can also compile to Lua 5.1 bytecode for embedding or to WebAssembly.
  • Compile-time metaprogramming features