Search code examples
luahookmetatable

Trying to make a lua proxy with metatables


I've read about metatables (here and here) and I asked myself if I could create a function call proxy by just adding a function with the same name to the __index table, so that it calls the index function (mine), and then I can call the normal one.

This is what I've tried:

local test = {}
test.static = function () print("static") end -- The normal function

local old = getmetatable(test) or {} -- Get the orginal meta
old.__index = {
    static = function () print("hook") end -- Adding my function
}

test.static() -- call just for test ( prints -> static )
test = setmetatable( test , old ) -- setting the metatable
test.static() -- call just for test ( prints -> static, but should be hook )

Solution

  • Try giving the official source a read, specifically §2.4 – Metatables and Metamethods, and the description of the __index methamethod, which reads:

    • __index: The indexing access table[key]. This event happens when table is not a table or when key is not present in table. The metamethod is looked up in table.

      Despite the name, the metamethod for this event can be either a function or a table. If it is a function, it is called with table and key as arguments, and the result of the call (adjusted to one value) is the result of the operation. If it is a table, the final result is the result of indexing this table with key. (This indexing is regular, not raw, and therefore can trigger another metamethod.)

    We can see that your thinking is backwards. Properties on a table referenced by the __index metamethod are only looked up if the original table does not contain the key.

    If you want to 'hook' a function you'll need to overwrite it, possibly saving the original to restore it later. If you want to tack functionally on to an existing function, you can write a neat little hooking function, which simply creates a closure around the functions, calling them in turn.

    local function hook (original_fn, new_fn)
        return function (...)
            original_fn(...)
            new_fn(...)
        end
    end
    
    local test = {}
    test.foo = function () print('hello') end
    test.foo = hook(test.foo, function () print('world!') end)
    
    test.foo() --> prints 'hello' then 'world!'
    

    Alternatively, you can switch between metatables, assuming the original table never overrides the keys of interest, to get different results:

    local my_table, default, extra = {}, {}, {}
    
    function default.foo () print('hello') end
    
    function extra.foo() print('world!') end
    
    local function set_mt (t, mt)
        mt.__index = mt
        return setmetatable(t, mt)
    end
    
    set_mt(my_table, default)
    my_table.foo() --> prints 'hello'
    set_mt(my_table, extra)
    my_table.foo() --> prints 'world!'