Proxy Based Encapsulation

lua-users home
wiki

The following technique can be used to achieve encapsulation of objects via proxy tables. The goal is to have a proxy object that exposes the methods for another object, the representation, but keeps the implementation hidden. The tricky (and as a result somewhat expensive) part is finding a way to store the link from proxy back to it's representation such that it can't be accessed by other code. Weak-keyed tables would seem like an option but semi-weak tables are prone to cyclic reference problems. The metatable would seem like an option but the metatable is either entirely hidden from Lua code or is entirely exposed. (I'm ignoring the option of writing a C-based implementation. Doing so would avoid this last problem.) So, the solution adopted here is to combine fully-weak tables with metatable-based references to make the link strong.

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.

See Also


RecentChanges · preferences
edit · history
Last edited March 14, 2009 5:57 pm GMT (diff)