Understanding Global Variables and Function Environments in Lua 5.1
In Lua 5.1, values of type thread, function, and userdata may each be linked to an environment table. That table is an ordinary Lua table used to hold names that the object resolves as "globals."
- Thread (coroutine) environments are only reachable from C.
- Userdata environments exist mainly for C-side association; Lua code generally ignores them.
- A function’s environment determines where its global-variable lookups and assignments go.
Note: This article targets Lua 5.1. In Lua 5.2 and later, getfenv/setfenv were removed and replaced by lexical _ENV.
Where global variables live
Names that are not declared local are stored in the environment of the running function. Standard library functions (e.g., setmetatable, string.find) are placed in the environment so Lua code can reference them as globals.
Working with function environments in Lua 5.1
Lua 5.1 exposes two primitives:
- getfenv([f]) — fetch the environment of function f (or of the current function if omitted). getfenv(0) returns the thread’s global environment.
- setfenv(f, env) — assign a new environment table to function f (or to the current function if f is omitted).
Listing all globals
getfenv(0) yields the global environment of the current thread. You can iterate it to inspect all global:
local globals = getfenv(0)
for name, value in pairs(globals) do
print("name:", name, "type:", type(value))
end
Defining global versus local variables
Names become global by default unless declared local. The following illustrates how global and local bindings appear from the environment:
local E = getfenv(1)
print("global_msg:", E.global_msg)
print("local_msg:", E.local_msg)
-- Assignments
global_msg = "hello from global"
local local_msg = "hello from local"
print("after assignment")
print("global_msg:", E.global_msg)
print("local_msg:", E.local_msg)
Example run:
$ lua test.lua
global_msg: nil
local_msg: nil
after assignment
global_msg: hello from global
local_msg: nil
local_msg is not present in the enviroment table, so E.local_msg is nil.
Environments of nested and non-nested functions
Functions created by load or loadstring at the top level are non-nested; by default, their environment is the thread’s global environment. When you define a function inside another function, the new function inherits its initial environment from the creating function.
local function outer()
print("outer env:", getfenv())
local function a()
print("a env:", getfenv())
end
local function b()
print("b env:", getfenv())
end
a()
b()
end
outer()
All three prints point to the same table, indicating that a and b initially share outer’s environment.
Replacing a function’s environment
Because a and b are created inside outer, they initially see the same environment. You can override one function’s environment using setfenv:
local function outer()
print("outer env:", getfenv())
local function a()
print("a env:", getfenv())
end
local function b()
-- This call will fail if b's environment does not provide getfenv
print("b env:", getfenv())
end
setfenv(b, {}) -- replace b's environment with a blank table
a()
b() -- triggers an error because getfenv is nil in b's new env
end
outer()
Sample error:
lua: test.lua:9: attempt to call global 'getfenv' (a nil value)
stack traceback:
test.lua:9: in function 'b'
test.lua:13: in function 'outer'
test.lua:16: in main chunk
[C]: ?
To call functions from the original environment after replacing b’s environment, capture the old environment as an upvalue and use it explicitly:
local function outer()
print("outer env:", getfenv())
local function a()
print("a env:", getfenv())
end
local base = getfenv() -- capture outer's environment
local function b()
local E = base -- alias to emphasize it's an upvalue
E.print("b env:", E.getfenv())
end
setfenv(b, {}) -- b now has an empty environment
a()
b() -- works via the captured base environment
end
outer()
Now b reports a different environment table than outer and a, because b’s environment was replaced.
The special table _G
_G is a variable that refers to the global (thread) environment. In a non-nested top-level chunk, printing _G or _G._G shows they reference the same table:
print(_G)
print(_G._G)
Example run:
table: 0xXXXXXXXX
table: 0xXXXXXXXX
If you replace a function’s environment with a table that does not define _G, then referring to _G inside that function yields nil:
local function outer()
local function g1()
print("g1:", _G)
end
local base = getfenv()
local function g2()
base.print("g2:", _G)
end
setfenv(g2, {})
g1() -- sees the global environment
g2() -- _G is nil in g2's new environment
end
outer()
Example run:
g1: table: 0xYYYYYYYY
g2: nil
GETGLOBAL (bytecode) vs lua_getglobal (C API)
Consider a simple script:
print("Hello")
Disassembling with luac -l shows Lua emits a GETGLOBAL for the print lookup (addresses and counts omitted):
main <test.lua:0,0>
GETGLOBAL 0 -1 ; print
LOADK 1 -2 ; "Hello"
CALL 0 2 1
RETURN 0 1
In Lua 5.1’s VM (luaV_execute), OP_GETGLOBAL conceptually does:
case OP_GETGLOBAL: {
TValue envtbl;
TValue *key = KBx(i); /* constant string name */
sethvalue(L, &envtbl, cl->env); /* envtbl = current function's env */
/* result = envtbl[key] pushed into register 'ra' */
Protect(luaV_gettable(L, &envtbl, key, ra));
continue;
}
Key point: GETGLOBAL looks up the name in the environment associated with the currently executing function (cl->env), not necessarily the thread’s global table.
By contrast, the C API function
void lua_getglobal(lua_State *L, const char *name);
fetches name from the thread’s global environment directly. Despite the similar naming, OP_GETGLOBAL consults the function’s environment, while lua_getglobal always consults the thread-global environment.