Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

Understanding Global Variables and Function Environments in Lua 5.1

Tech 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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.