Runtime#

The runtime is the interpreter, the module system, the environment, the garbage collector, and the C API that glues Lua to host programs. The operator’s source is bytecode-compiled on load; that bytecode runs on either PUC-Rio Lua (the reference) or LuaJIT (the production interpreter inside OpenResty and most game engines).

For the language itself, see Syntax. For the toolchain that drives the runtime (lua, luajit, luarocks), see Tooling.

Interpreters#

Interpreter

Notes

PUC-Rio Lua (lua)

The reference implementation. 5.4 is current; many embedded hosts still pin 5.1.

LuaJIT (luajit)

Tracing JIT, two orders of magnitude faster on hot code. Language level is Lua 5.1 with some 5.2 / 5.3 extensions; the operator targets this version when writing for OpenResty, HAProxy, or most game engines.

eLua / NodeMCU

PUC-Rio Lua trimmed for microcontrollers (ESP32, ESP8266).

MoonScript / Fennel / Teal

Languages that compile to Lua. The operator meets them inside specific ecosystems (LÖVE2D, Neovim) but they rarely cross over.

lua -v prints the version. _VERSION (a global) and jit.version (LuaJIT only) do the same at runtime.

$ lua -v
Lua 5.4.6  Copyright (C) 1994-2023 Lua.org, PUC-Rio

$ luajit -v
LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2023 Mike Pall.

Loading and require#

require(name) is Lua’s module-loading primitive. It searches package.path for .lua files and package.cpath for .so / .dll shared libraries, loads the first match, caches the result in package.loaded, and returns whatever the module returned (conventionally a table).

local socket = require("socket")
local sock = socket.tcp()

The module file is a chunk like any other; whatever it returns becomes the module value.

-- mymod.lua
local M = {}

function M.greet(name)
  return "hello, " .. name
end

return M

A second require("mymod") returns the cached table; the file is loaded exactly once per process.

package.path#

package.path is a semicolon-separated list of ? templates. The ? is replaced with the module name (with dots converted to /).

print(package.path)
--> /usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;...

require("foo.bar")
--> tries /usr/local/share/lua/5.4/foo/bar.lua
--> then  /usr/local/share/lua/5.4/foo/bar/init.lua
--> then  the next template in the chain

package.cpath is the same for C modules. The operator extends both at startup when working outside the system path.

$ LUA_PATH=";;./?.lua;./vendor/?.lua" lua main.lua

The double-semicolon expands to the original default; the operator then prepends project-local paths.

_G and _ENV#

_G is the global environment table. _ENV is the per-chunk environment that the compiler implicitly references for every unqualified name. In a normal chunk _ENV and _G point at the same table; you can swap _ENV to sandbox a chunk.

x = 1
print(_G.x)        --> 1
_G.y = 2
print(y)           --> 2

Sandbox a chunk by replacing _ENV.

local sandbox = {print = print, math = math}
local chunk = load(user_code, "user", "t", sandbox)
if chunk then chunk() end

The chunk now sees only print and math; os, io, require, and globals are invisible.

load and loadfile#

load(s) compiles a string into a callable chunk; loadfile does the same for a file path; loadstring is the 5.1 name for load. All return nil, err on syntax errors.

local chunk, err = load("return 1 + 2")
if not chunk then error(err) end
print(chunk())              --> 3

load is the only way to evaluate strings at runtime; Lua has no eval.

Garbage collection#

Lua uses an incremental, mark-and-sweep collector. The operator rarely touches it; collectgarbage("count") reports kilobytes in use; collectgarbage("collect") forces a full cycle.

Weak tables let the operator break reference cycles for cache- style data. The __mode metatable field controls which side is weak: "k" for weak keys, "v" for weak values, "kv" for both.

local cache = setmetatable({}, {__mode = "v"})
cache.user42 = compute()         -- collected if nothing else holds it

__gc (5.2+) is the finaliser metamethod. Tables with a metatable that has __gc get one final call before collection. Use it to release native resources (file handles, sockets) acquired through userdata wrappers.

The C API#

Lua’s C API is the boundary between the runtime and a host program. The host pushes values onto a stack, calls Lua functions, and reads results back. lua_State *L is the handle; lua_pushinteger, lua_call, lua_tostring, lua_newtable, and friends are the verbs.

The operator rarely writes raw C against the API; they write either pure Lua modules or use a binding generator (swig, lua-cdef, LuaJIT’s ffi).

LuaJIT’s FFI is the friendliest path. It accepts C declarations in a string and exposes the symbols directly.

local ffi = require("ffi")
ffi.cdef[[
  int printf(const char *fmt, ...);
]]
ffi.C.printf("hello %d\n", 42)

Use FFI to call libcurl, libsodium, or any other .so already on the host without writing a binding shim.

Modules and packages#

A module is a .lua file (or init.lua inside a folder) that returns a value, conventionally a table. A package is a collection of modules under a single name namespace. LuaRocks is the standard package manager (see Tooling).

project/
  main.lua
  scan/
    init.lua            -- require("scan")
    tcp.lua             -- require("scan.tcp")
    udp.lua             -- require("scan.udp")
-- main.lua
local scan = require("scan")
local tcp  = require("scan.tcp")

scan.run()
tcp.probe("10.0.0.1", 22)

The module-as-returned-table pattern is the operator’s standard. Avoid global side-effects in a module body; another require has no way to opt out.

References#