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 ( |
The reference implementation. 5.4 is current; many embedded hosts still pin 5.1. |
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.