Proxy Based Encapsulation |
|
local function noProxyNewIndex() error "Cannot set field in a proxy object" end function makeEncapsulator() -- Returns two tables: The first converts from representations to proxies. The -- second converts the other way. The first will auto-generate new proxies. local proxy2rep = setmetatable( {}, { __mode = "kv" } ) local rep2proxy = {} -- This will be made weak later, but we need to construct more machinery local function genMethod( methods, k ) -- Index function for the __index metatable entry local result = function( proxy, ... ) local rep = proxy2rep[ proxy ] return rep[ k ]( rep, ... ) -- Lua 5.1! end methods[ k ] = result return result end local proxyIndex = setmetatable( {}, { __index = genMethod } ) -- __index table for proxies local function makeProxy( rep ) local proxyMeta = { __metatable = "< protected proxy metatable >", rep = rep, -- GC protection, we won't be able to read this __index = proxyIndex, __newindex = noProxyNewIndex } local proxy = setmetatable( {}, proxyMeta ) proxy2rep[ proxy ] = rep rep2proxy[ rep ] = proxy return proxy end setmetatable( rep2proxy, { __mode = "kv", __metatable = "< protected >", __index = function( t, k ) local proxy = makeProxy( k ) t[ k ] = proxy return proxy end } ) return rep2proxy, proxy2rep end
Usage is as follows. We make an encapsulator and then run objects in need of encapsulation through it. Clients must be careful to encapsulate an object whenever it crosses the encapsulation barrier since self is equal to the real object rather than the proxy inside any methods.
local encapsulator = makeEncapsulator() local foo = { hello = function(self) print("Hello from " .. tostring(self)) end } print("foo = " .. tostring(foo)) local efoo = encapsulator[foo] print("efoo = " .. tostring(efoo)) local efoo2 = encapsulator[foo] print("efoo2 = " .. tostring(efoo)) efoo:hello() local baz = { hello = function(self) print("Greetings from " .. tostring(self)) end } print("baz = " .. tostring(baz)) local ebaz = encapsulator[baz] print("ebaz = " .. tostring(ebaz)) ebaz:hello()
Note that makeEncapsulator
returns the tables that go in both directions. The second table is of use if one needs to penetrate the proxy barrier for objects other than the target of a method call.
Note that one should not expose the encapsulator tables to untrusted code. The proxy2rep
table is clearly dangerous since it grants more or less direct access to the representations. The rep2proxy
table is dangerous because it can be iterated. That could be addressed by wrapping it another level of proxy table, but that would make encapsulating objects more expensive. One could also wrap the table in a function but again this would run more slowly.
Nothing in this implementation stands in the way of using a single encapsulator tables for multiple object types. The chief reasons to use multiple encapsulators would be multiple contexts that shouldn't be able to see each others reps. There may also be speed advantages in having multiple smaller tables rather than having a few large tables.