Skip to content

Instantly share code, notes, and snippets.

@nefftd
Created October 8, 2014 02:57
Show Gist options
  • Save nefftd/1bb88ded0875e27964c7 to your computer and use it in GitHub Desktop.
Save nefftd/1bb88ded0875e27964c7 to your computer and use it in GitHub Desktop.
-- Problem: it appears to be impossible to protect a metatable from being unset
-- while also allowing Lua-side code to read/write metamethods. The solution is
-- to use a proxy.
-- The idea is, we use __metatable so that getmetatable() returns a proxy
-- object which inherits from the real metatable. User code can read/write
-- fields to the real metatable through this proxy.
-- Note: Keep __metatable intact on the proxy to (1) protect user code from
-- changing the behavior of the proxy and (2) protect user code from getting
-- access to the real metatable via getmetatable(proxy).__index
-- READ ONLY FORM
do
local function throw_write_error(self,k)
k = tostring(k)
error("attempt to alter protected metatable (with key '"..k.."')",2)
end
function setprotectedmt(tbl,meta)
local meta_proxy = setmetatable({},{
__index = meta,
__newindex = throw_write_error,
__metatable = false,
})
meta.__metatable = meta_proxy
return setmetatable(tbl,meta)
end
end
-- Test
local test = {}
local mt = { __newindex = print }
setprotectedmt(test,mt)
assert(getmetatable(test).__newindex == print)
assert(not pcall(function() getmetatable(test).__newindex = function() end end))
assert(not pcall(setmetatable,test,nil))
test.a = 1 -- prints <table: n>, 1
-- WRITE NEW ONLY FORM
-- This form protects any fields present at the time the metatable was set. All
-- other fields can be written to.
do
local function throw_write_error(k)
k = tostring(k)
error("attempt to alter protected metamethod '"..k.."'",3)
end
function setprotectedmt(tbl,meta)
if type(meta) ~= 'table' then
error("bad argument #1 to 'setprotectedmt' (table expected)",2)
end
local protected = {}
for k in next,meta do
protected[k] = true
end
local meta_proxy = setmetatable({},{
__index = meta,
__newindex = function(self,k,v)
if protected[k] then throw_write_error(k) end
meta[k] = v
end,
__metatable = false,
})
meta.__metatable = meta_proxy
return setmetatable(tbl,meta)
end
end
-- Test
local test = {}
local mt = { __newindex = print }
setprotectedmt(test,mt)
assert(getmetatable(test).__newindex == print)
assert(not pcall(function() getmetatable(test).__newindex = function() end end))
assert(pcall(function() getmetatable(test).__custom = true end)) -- can write custom field?
assert(not pcall(setmetatable,test,nil))
test.a = 1 -- prints <table: n>, 1
-- __protected FORM
-- This form expects a custom __protected on the *real* metatable. If present,
-- any field in that table will be protected from writes. All other fields can
-- be safely written.
do
local function throw_write_error(k)
k = tostring(k)
error("attempt to alter protected metamethod '"..k.."'",3)
end
function setprotectedmt(tbl,meta)
local meta_proxy = setmetatable({},{
__index = meta,
__newindex = function(self,k,v)
if k == '__protected' or (meta.__protected and meta.__protected[k]) then
throw_write_error(k)
end
meta[k] = v
end,
__metatable = false,
})
meta.__metatable = meta_proxy
return setmetatable(tbl,meta)
end
end
-- Test
local test = {}
local mt = { __newindex = print, __protected = { __newindex = true } }
setprotectedmt(test,mt)
assert(getmetatable(test).__newindex == print)
assert(not pcall(function() getmetatable(test).__newindex = function() end end))
assert(pcall(function() getmetatable(test).__custom = true end)) -- can write custom field?
assert(not pcall(setmetatable,test,nil))
test.a = 1 -- prints <table: n>, 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment