Search code examples
luametatable

How to log nested table accesses/writes?


As part of a project I was working on, I wanted to be able to print out whenever a table is accessed or written to, and what was accessed/written to in the table. Looking this up, I found this, which described how to track whenever a table is accessed/updated by using a proxy table and the __index and __newindex metamethods. However, the code they provide does not properly display what is going on if a nested table is used as the proxy table. Suppose I write the following code, adapted from the previous one:

mt = {}
function mt.__index(self, key)
  print('accessing key '..key)
  return self.proxy[key]
end
function mt.__newindex(self, key, value)
  print('setting key '..key..' to value '..tostring(value))
  self.proxy[key] = value
end

function setproxy(t)
  new_t = {proxy = t}
  setmetatable(new_t, mt)
  return new_t
end

t = {
  a = 1,
  b = 2,
  c = {
    a = 3,
    b = 4,
  },
}

t = setproxy(t)
t.a = 2 -- prints "setting key a to value 2" as expected
t.c.a = 4 -- prints "accessing key c", nothing else

The problem here is that __index is called to access the key c and it returns a value in the proxy table, but that doesn't have the same metatable so it doesn't log the write to t.c. I would like for the second assignment to print something like setting key c.a to value 4, but I'm not really sure where to start on actually implementing such a thing.

After a lot of thought, I think you might be able to do it by having each key that has a table value also be another proxy table, but then you have to

  • recursively replace all table values with proxy tables, and I was thinking each proxy table would include some info that allows __newindex for this proxy table to print out the correct key
  • if some key is set to a table value, you have to recursively replace it with proxy tables before you can set the actual value

and that just seems like so much work and complication for something that should be simpler than this.


Solution

  • What you need is a proxy-table for every table you want to access. The simplest way of doing this, though not the most performant would be returning a new proxy-table whenever the original proxy is access and a normal table would be returned:

    mt = {}
    function mt.__index(self, key)
      print('accessing key '..key)
      local value = self.proxy[key]
      if type(value)=='table' then
        return setmetatable({proxy=value}, mt)
      else
        return value
      end
    end
    function mt.__newindex(self, key, value)
      print('setting key '..key..' to value '..tostring(value))
      self.proxy[key] = value
    end
    
    function setproxy(t)
      new_t = {proxy = t}
      setmetatable(new_t, mt)
      return new_t
    end
    
    t = {
      a = 1,
      b = 2,
      c = {
        a = 3,
        b = 4,
      },
    }
    
    t = setproxy(t)
    t.a = 2 -- Works as expected
    t.c.a = 4 -- Also works as expected
    

    A note on performance:

    Since tables in Lua are garbage-collected, creating new tables is generally considered "slow". This is still a matter of perspective though; if you're writing a simple script that you run by hand, don't bother optimizing, it's still gonna be very fast. If you're writing nested loops with millions of iterations or if your code needs to respond within as few milliseconds as possible, then you should consider caching these proxy tables instead, depending on your use case. If you find your code accessing the same proxied tables again and again, creating new proxy-tables each time, you could cache them in a proxies table, where proxies[table_A] == proxy_to_A and set a __index metamethod that creates the proxy if it doesn't exist. (At this point the tradeoff is that creation of new proxies might be slightly slower because of the metamethod invocation).