Namespace isolation "patch" for troublesome functions

Post Reply
Sanaki
Posts: 110
Joined: Wed Mar 09, 2011 1:30 am

Namespace isolation "patch" for troublesome functions

Post by Sanaki »

First off, this isn't for beginners. It uses a few more advanced concepts to implement what it does. Use carefully, and ask if you're unsure of how to use it. When in doubt, use the premade function at the end.

So, many times now I've run into functions I've tested for others with... problems. Specifically, many functions have been made primarily by beginning coders which create a large number of variables outside of a namespace, sometimes overwriting important functions or otherwise mussing up the global environment. For the odd times when rewriting these isn't reasonable, feasible, or possible for whatever reason, I wrote up a little patch script.

First off, you'll need the deepcopy function from http://lua-users.org/wiki/CopyTable. I've included it below, with the adjustment to deepcopy metatables as well:
Code: [show] | [select all] lua
function deepcopy(object)
	local lookup_table = {}
	local function _copy(object)
		if type(object) ~= "table" then
			return object
		elseif lookup_table[object] then
			return lookup_table[object]
		end
		local new_table = {}
		lookup_table[object] = new_table
		for index, value in pairs(object) do
			new_table[_copy(index)] = _copy(value)
		end
		return setmetatable(new_table, _copy(getmetatable(object)))
	end
	return _copy(object)
end
From there, let's say we have a function named exfunc. More often than not when I find need of this, it's instead a larger script which I've wrapped in the function. Either is fine for this example.
Code: [show] | [select all] lua
-- This table is your new working environment for this function
-- This can be a local if you don't want persistent changes
-- Change the name as you please on a per-function basis, here and in the setmetatable and setfenv calls
exenv = {}

-- This table is the new metatable for the environment
-- Don't adjust this without knowing what you're doing
local envmeta = {
	-- Ensure transparent access to all globals
	__index = _G,
	-- Ensure new writes don't affect global environment's tables
	__newindex = function(t, k, v)
		rawset(t, k, deepcopy(v))
	end
	}

-- Set the metatable for the environment to allow the magic to work
setmetatable(exenv, envmeta)

-- Set the default environment of the function to our new one
setfenv(exfunc, exenv)
The end result is that your original function now runs cleanly, still writes all it wants, and doesn't mess up anything for other scripts. All variables, functions, etc created will actually be created in the exenv table, the function will have no basic ability to change globals, it will retain the ability to access all global functions, variables, tables, etc without issue, and all changes made can be reversed simply by using this:
Code: [show] | [select all] lua
setfenv(exfunc, getfenv(0))
The setfenv function returns the function on execution, allowing setfenv(exfunc, exenv)() to be used if immediate execution is desired. This isn't required, the environment change is applied to the function persistently.

Worth note: This isn't completely foolproof technically. Using rawset can bypass the deepcopy protection, changing the local environment will quite obviously mess this up, and changes to the environment's metatable will interfere with it as well. On the other hand, if the script is doing any of these things, chances are the author should know better than to write in such a way where this is necessary.

It's possible to wrap this all into a simple function to implement quickly as well, like so:
Code: [show] | [select all] lua
function isoenv(f, t)
	local function deepcopy(object)
		local lookup_table = {}
		local function _copy(object)
			if type(object) ~= "table" then
				return object
			elseif lookup_table[object] then
				return lookup_table[object]
			end
			local new_table = {}
			lookup_table[object] = new_table
			for index, value in pairs(object) do
				new_table[_copy(index)] = _copy(value)
			end
			return setmetatable(new_table, _copy(getmetatable(object)))
		end
		return _copy(object)
	end

	local newenv
	if type(t) == "table" then
		newenv = t
	elseif type(t) == "string" then
		local newenv = _G[t] or {}
		_G[t] = newenv
	else
		error("Function Isolation failure:\nTables supplied must be either a table or a string.", 0)
	end

	local isofunc
	if type(f) == "function" then isofunc = f
	elseif type(f) == "string" then isofunc = loadstring("return "..f)()
	else error("Function Isolation failure:\nFunctions supplied must be either a function or a string.", 0)
	end

	local envmeta = {
		__index = _G,
		__newindex = function(t, k, v)
			rawset(t, k, deepcopy(v))
		end
		}
	setmetatable(newenv, envmeta)
	setfenv(isofunc, newenv)
end
Using the above, the same earlier code could be accomplished like so:
Code: [show] | [select all] lua
isoenv("exfunc", "extable")

Post Reply