Function Environments Explained

Introduction

A little while ago I came across this tutorial explaining getfenv() and setfenv(). So why am I making this tutorial when that one already exists? Well they did a pretty awful job explaining and following that tutorial is bad practice.

Function Environments

Function environments are environments where your code runs. At their most basic/tangible form they are tables. Let’s see an example of a script:

local a = 5
b = 3

The environment of this script defines b as 3 since the variable is global. Since a is local and not global if you attempt to print the contents of this environment you’ll see it isn’t visible.

Function environments work using a sort of layer-system. Let’s say we had the following code:

-- Layer 2 relative to b
x = 99

function a() -- Layer 1 relative to b
  function b() -- Layer 0 relative to b (itself)
    
  end
end

So if you want to go out of a deeply nested function you simply start at 0, the starting environment, and work your way out. This will be better explained when we look at interacting with environments.

Interacting with Environments

Lua provides two useful functions for interacting with function environments. They are getfenv() and setfenv().

getfenv()

You can get a function environment using this function. Let’s say we wanted to print a function environment:

local a = 5
b = 3

print(getfenv(0))

image

Just as explained above b is accessible but a is not. You’ll also notice script. In Roblox when you attempt to reference a script’s instance you use the script keyword. Function environments are how Roblox makes the keyword accessible.

Environments also contain a metatable. However, these are locked and not accessible in Roblox:

print(getmetatable(getfenv(0))) --> Outputs: The metatable is locked

That said, you can still alter the metatables of environments. One potential use case for this is the creation of “custom” enums. In this example I’ll just make it so if you attempt to print the Enum global it prints Hello World!:

local originalEnum = getfenv(0).Enum -- Store original Roblox-defined enums
getfenv(0).Enum = setmetatable({}, {
  __tostring = function()
    return 'Hello World!'
  end,
  __index = function(_, k) -- Preserve Roblox-defined enums
    return originalEnum[k]
  end
})
print(Enum) --> Outputs: Hello World!

Note: This is happening within the script’s function environment alone. What this means is other scripts will maintain their original environments.

setfenv()

You can set a function environment using this function. Let’s say we wanted to override the existing function environment:

y = 30
setfenv(0, { y = 2 })
print(getfenv(0)) --> Outputs: { ["y"] = 2 }

That’s pretty much all there is to it. Keep in mind using setfenv() overrides the entirety of an environment. This is where referencing a value within getfenv() can be useful. E.g.

u = 45
getfenv(0).SomeValue = 15
print(getfenv(0)) --> Outputs: { ["SomeValue"] = 15, ["u"] = 45 }

Notice how the variable u is preserved.

Modules & Environments

When you require() a module the module executes code in its own environment. However, the value it returns becomes a part of the environment that require()ed it. So for example,

-- Script
local x = require(script.ModuleScript)
x.DoStuff()
print(abc) --> Outputs: Hey!
-- ModuleScript
local module = {}

function module.DoStuff()
  getfenv(2).abc = 'Hey!' -- 2 is the layer the original script is under, 1 is the module script, 0 is this function
end

return module

Conclusion

I hope this tutorial helped explain function environments and you now understand how powerful interacting with these environments can be. These are majorly overlooked when scripters start learning Lua when they should be one of the first concepts learned.

Warning: Interacting with function environments in Roblox will disable Luau’s built-in optimizations due to it being considered untrusted code as seen here.

6 Likes

Following this tutorial is also bad practice though. New work should never be using function environments; not only do they disable Luau optimisations but they’re awful in terms of code structuring. Use ModuleScripts instead if you need to import variables, functions and other code into your environment. Additionally be sure to use local variables, not globals.

Learning about scopes and environments isn’t necessarily the first thing programmers need to learn but it does help in understanding how your code comes together. Getting and setting function environments is irrelevant, unnecessary and bad for Roblox programming and should be avoided.

EDIT: It’s been pointed out below too but this is why it’s important to do research before to other people’s comments (referring to second reply on my post). Source for claim. Simply calling getfenv/setfenv disables Luau optimisations. Never use these functions in new production-level work.

1 Like

This is more of an explanation than a tutorial for people who are uneducated on function environments. That said I was unaware interacting with the environment caused issues with Luau optimizations. I’ll make an edit now.

Passing 0 to getfenv or setfenv uses the thread’s environment, not the function’s environment. 1 refers to the caller of getfenv or setfenv, 2 refers to the caller of the caller of getfenv or setfenv, and so on. There is no notion of a “layer system”, the index refers to a function in the call stack. Functions may also be passed directly to getfenv or setfenv. getfenv defaults to 1 if no value is passed.

Here is an example of using function environments and reading the environment of a function from the callstack:

local print = print
local setfenv = setfenv
local getfenv = getfenv
local function f(i)
	if i == 100 then
		print(getfenv().x)
		print(getfenv(100).x)
		print(getfenv(101).x)
		print(getfenv(0).x)
	else
		f(i+1)
	end
end
setfenv(1,{x=1})
setfenv(f,{x=2})
f(1)
--> 2
--> 2
--> 1
--> nil
1 Like

getfenv used specifically for reading is intrinsically safe and doesn’t deoptimize the luau environment. It is used in Adopt Me for logging purposes.

See: Function Environments Explained - #12 by Ukendio

1 Like

Since it can’t read local variables, it does implicate that you need to use the more frowned upon global variables.

The second you call getfenv the acquired environment is “tainted”. When you access math in that environment you can no longer guarantee that you are infact accessing the math library. It’s probably best to stay away from these functions

3 Likes

Expect it does penalize access to built-in globals and does disable optimizations on some built-in functions like math.max:

getfenv previously deoptimized the environment because it returns a mutable table. As said, using getfenv strictly for read-only purposes is intrinsically safe. It is commonly used for diagnostics and logging purposes.

@incapaz don’t write to it and it won’t taint the environment.

Calling getfenv in any of point of a code will deoptimize the thread it’s called in even if you use it for read-only purposes or do nothing with it at all and you can test that by yourself:

local clock = os.clock()
for i = 1, 1000, 1 do
	math.max(math.random(1, 5), math.random(1, 5), math.random(1, 5))
end
print(os.clock() - clock)

getfenv()

local clock = os.clock()
for i = 1, 1000, 1 do
	math.max(math.random(1, 5), math.random(1, 5), math.random(1, 5))
end
print(os.clock() - clock)
--Output
-> 7.0699999923818e-05  (Which is around 0.000070699999923818)
-> 0.00019599999995989  (Which is around 0.000195999999959890)
--Numbers may differ for your case but 1st printed number is always lower than the 2nd one.
2 Likes

I don’t think that’s true:

sauce

3 Likes

You guys are all correct and I was wrong! @BenMactavsin @incapaz

Asked one of our favourite engineers about this and he responded very quickly with

He also tweeted about it :slight_smile:

2 Likes