It is common practice to make a new table, userdata, or (rarely) a function when you need a new unique value that cannot be obtained from anywhere else except by passing it around. For example, someone were to create a function that prints its input in either the standard output or error output they could naively implement it like so:
local function printMsg(value, ...)
if value == 'error' then
error(...)
else
print(value, ...)
end
end
However, if my message that I want to print is ever ‘error’ then this function will actually throw an error and drop that portion of my message! While we could pass in a callback in this example, sometimes it is advantageous to pass in a unique value instead. The function could be implemented like so:
local ERROR = newproxy() -- or {} or function() end
local function printMsg(value, ...)
if value == ERROR then
error(...)
else
print(value, ...)
end
end
A more practical use case would be in information hiding and secret exchange. Consider this locked down script:
local PROCESS_ACCESS_KEY = newproxy()
local SERVICE_ACCESS_KEY = newproxy()
local KERNEL_ACCESS_KEY = newproxy()
local ACCESS_LEVEL = {
[PROCESS_ACCESS_KEY] = 1;
[SERVICE_ACCESS_KEY] = 2;
[KERNEL_ACCESS_KEY] = 3;
}
require('scheduler')(KERNEL_ACCESS_KEY)
local services = {
require 'anti-exploit';
require 'IPC';
}
local processes = {}
for i, service in ipairs(services) do
service.setKey(SERVICE_ACCESS_KEY)
end
function shared.startProgram(accessKey, program)
if ACCESS_LEVEL[accessKey] >= ACCESS_LEVEL[SERVICE_ACCESS_KEY] then
local process = coroutine.create(program)
processes[process] = tick()
else
error('Access Denied.', 2)
end
end
function shared.stepScheduler(accessKey)
if ACCESS_LEVEL[accessKey] >= ACCESS_LEVEL[KERNEL_ACCESS_KEY] then
for i, service in ipairs(services) do
service(SERVICE_ACCESS_KEY)
end
for i, process in ipairs(processes) do
coroutine.resume(PROGRAM_ACCESS_KEY)
end
end
end
-- In scheduler script
return function(accessKey)
spawn(function ()
while wait() do
shared.stepScheduler(accessKey)
end
end)()
end
Now in the previous examples, the unique value could be table or function without any issues. The advantage of tables over functions is that you can set their metatable. This allows you not only to have unique values, but unique values that print prettily because they have a custom __tostring method on them. You can also define + to do special operations like return another table which represents the union of the two tables.
However, tables do have a problem. Consider this example:
local Set = {}
Set.__index = Set
function Set.new(data)
return setmetatable(data, Set)
end
function Set:__add(other)
local result = {}
for key, value in next, self do
result[key] = value
end
for key, value in next, other do
result[key] = value
end
return result
end
return setmetatable(Set, {
__metatable = 'This metatable is locked';
__newindex = {}; -- don't change me
})
-- In another script:
local playerScores = Set.new {}
...
local function adjustScores(adjustment)
playerScores = playerScores + adjustment
end
-- An in some module that got inserted and required at run time:
local Set = require 'Set'
local Spy = {}
function Spy.new(data)
data.IdiomicLanguage.moneyz = 9999999999999
return data
end
function Spy:__add(other)
other.IdiomicLanguage = nil -- Don't change my moneyz please
return self, other
end
for key, newValue in next, Spy do
local oldValue = Set[key]
rawset(Set, key, function(...)
return oldValue(newValue(...))
end)
end
Because rawset can be used on tables, the desired functionality of tables can be changed. Since __index only runs when the key being accessed doesn’t exist in a table, rawset can be used to prevent the __index or __newindex metamethod from running. Eventually you run into this huge problem where even if you don’t have some nefarious code running on your server, if you ever use rawset then you can get into huge trouble with crazy bugs if it is used on the wrong table/key.
Because userdata’s don’t store keys/values, rawset and rawget don’t affect them. With a locked metatable, you have absolute control and security. __index will fire every time the userdata is accessed, and __newindex every time a value is set. The metatable cannot be obtained. The functions and methods on it can’t even be enumerated. It is essentially a black box to any code looking at it.
Another advantage of a userdata is that it isn’t a table. For example, if you have a function that usually takes in tables then if you were to pass in unique tables to perform special actions, you would have to be able to compile a list of special tables before hand and compare the each table you receive to the list. On the other hand, with a userdata value you can simply check the type and if it is a userdata, then call it (the __call metamethod), add it, or whatever, without having to compile a list to know that it was a special value.
The original purpose if userdata values is to provide an interface between Lua and C/C++. That is why all instances are userdatas. I’m not as familiar with this aspect of it, so I’ll forgo expending on this topic (although there are probably some brilliant people here who could).