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.

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

4 Likes

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
3 Likes

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

5 Likes

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

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.
3 Likes

I don’t think that’s true:

sauce

5 Likes

hello
can i ask what does the 0 number in the getfenv(0) function mean?

It returns the current thread’s env of the script.

Sorry for bumping.

Great tutorial but getfenv() and setfenv() is not great if you use it as @colbert2677 it can disable Luau optimizations and are awful in code structuring.

getfenv() and setfenv() have limited use cases you can use it in ways, but it is generally limited.

I do like experimenting and seeing what I can create with getfenv() and setfenv() though.

It might even get deprecated or become legacy in Roblox in the future however take that as not confirmed as Roblox (I don’t think) have deprecated it or has become legacy yet.

Overall, you can use ModuleScripts instead like @colbert2677 said but if you need to use getfenv() and setfenv() in a way then go for it no one is stopping you just beware of the risks you can face with it.

2 Likes