Open-source code to get started on feeding your learning about get/setfenv

Don’t use getfenv/setfenv in new work.

This resource is outdated and based on old knowledge. This resource was written purely because I found it cool that a certain asset was able to use custom globals so I wanted to figure out how I could do that myself and share my knowledge.

Following the “official release” of Luau, there are only very niche cases where setting function environments is necessary. Most new work should not need to use it and any use cases for it should be resolved with ModuleScripts. Environments can become impure by setting or even just getting them and various optimisations Luau provides will be nullified.

It’s also important to note that there’s an RFC for deprecating getfenv/setfenv in Luau that has been pulled to the repository, so it’s likely there will be official recommendation against using them.

See: Importing global access chains / Deprecate getfenv/setfenv


Please read before proceeding.

This is not a reliable resource and should not be used anymore I created this in 2018 with a lack of strong knowledge about function environments and made this on the basis of “I think it’s cool an asset is able to use custom globals”, that’s it. This resource is merely a primer for using getfenv/setfenv but it does not explore the concept of function environments deeply. Additionally, please see the above section exploring use of these in code.


Content

You will most likely not have any reason to use get/setfenv. I know I don’t. I made a model anyway and I open-sourced it some time back. I’m bringing it to the DevForum because I want to have a post in Community Resources and I feel like sharing this. Also in that respect, if you ever find yourself needing to use either get/setfenv, you have this which you can read, salvage or do whatever you want. You could even make a framework or system set up in this fashion.

Here you go: [OUTDATED] API Example - Roblox

(AS OF 2022 MAY 30, I have taken this model offsale. I do not want to encourage bad practice and I have no intentions to update either this resource or the thread.)

No, that isn’t really API. It’s a script I created after referencing what SyncAdmin did to have their commands work. In their plugins (commands, both ones that come with the model and ones that you can write), you can use certain methods off of “SyncAPI”. It gives a blue underline because it’s like an unknown global variable in-script, but methods actually work because SyncAPI is defined via setfenv in a private-source module.

SyncAPI.SomeFunctionFromTheAPI

^ Without setfenv, that normally wouldn’t work.

Leave feedback, comments or whatever below. Feel free to message me if you notice something off about my code as well - I am still learning and far from having a good arsenal of scripting knowledge and power.

18 Likes

That’s pretty cool that you’ve taken the time to look into SyncAdmin. We do indeed use getfenv() and setfenv() for our custom API which allows game developers to create their own custom commands.

However, in addition to this, we also use metatables to lock the API table to prevent plugins from modifying the SyncAPI table.

This can be done by doing something like:

local apiInternal = {} --This is the table containing all the functions

--This is an example API function
function apiInternal.ExampleFunction(...)
    print("Example called with: ", ...)
end

local apiExposed = setmetatable({},{
    --This function is called whenever a script tries to read a value from apiExposed
    __index = function(_,key)
        --The function returns the value the read attempt should give
        return apiInternal[key]
    end;

    --This function is called whenever a script tries to write a new value to apiExposed
    __newindex = function(_,key,value)
        --We don't want people setting anything here, so just throw an error
        error("Cannot write to CustomAPI")
    end;

    --This table is returned whenever someone tries to read the metatable of apiExposed
    __metatable = {};
})

function test()
    CustomAPI.ExampleFunction("Hello","World")
end

local env = getfenv(test)
env.CustomAPI = apiExposed
setfenv(test,env)

It’s important to note that index and __index and __newindex are only called if that value doesn’t exist in the table the scripts are attempting to read/write. In our example, we pass an empty table as the first argument to setmetatable. setmetatable returns that empty table after setting its metatable, so apiExposed doesn’t actually contain any of the functions or values. It’s just a proxy to access the values in apiInternal.

Thanks!
~ ForPizzaSake & VolcanoINC

15 Likes

I know that this is an old topic, but I’ve found that newproxy(true) is a better way to lock the table to prevent modifications to it. This is because a userdata cannot be directly edited-you need its metatable. This is useful because then the API isn’t even a table, so you aren’t risking having any modifications made to currently existing functions. As the code currently is, the currently existing API functions can be modified, to an extent. I believe I’ve constructed a viable solution to this. Let me know if I’m incorrect, but I’ve been working with the SyncAdmin structure for a long time ever since it shut down, and I think this would be a better alternative to using just metatables:

EncryptTable = function(securityContext)
	local userdata,metaData
	
	userdata = newproxy(true)
	metaData = getmetatable(userdata)
		
	for field,value in pairs(securityContext) do
		metaData[field] = value
	end
		
	return userdata
end

Your advice is about as old as this topic as well. Release 498 introduced table.freeze which will truly lock a table from any modifications, mostly getting rid of the need for a proxy.

If you only need to prevent modifications to the source table then you should use table.freeze. You’ll be able to continue working with the original table without introducing an extra item in memory and since “the VM has a first class concept of a frozen table, it can’t be modified no matter what hacks you pull off”.

Worth adding on that table.freeze will not recursively freeze.

See this thread for information and discussion:

4 Likes