Lua 5.1 workaround for __gc metamethod for tables

4k views Asked by At

I'm currently facing the problem that you can't use the __gc method for tables in Lua 5.1, as they are implemented in Lua 5.2. However, I want to release allocated native resources once the lua table gets collected. Is it possible to make a workaround which gives me the functionality of __gc metamethod in Lua 5.2 for Lua 5.1?

2

There are 2 answers

2
greatwolf On BEST ANSWER

In lua 5.1 the only lua values that work with __gc metamethod is userdata. Naturally any hack or workaround will have to involve userdata in someway. Normally there is no way to just create newuserdata from the lua side but there is one "hidden" undocumented function newproxy for doing just that.

newproxy takes an optional bool or userdata parameter. If you pass in true then you get a userdata with a new metatable attached. If you pass in another userdata then the new userdata will be assigned the same metatable as the one passed in.

So now you can just hack together a function that'll make __gc work on tables:

function setmt__gc(t, mt)
  local prox = newproxy(true)
  getmetatable(prox).__gc = function() mt.__gc(t) end
  t[prox] = true
  return setmetatable(t, mt)
end

And a quick test to confirm the behavior:

iscollected = false
function gctest(self)
  iscollected = true
  print("cleaning up:", self)
end

test = setmt__gc({}, {__gc = gctest})
collectgarbage()
assert(not iscollected)

test = nil
collectgarbage()
assert(iscollected)

IDEOne Demo

Note that lua 5.2+ and later no longer have newproxy since __gc is officially supported on tables.

0
River On

__gc support in setmetatable for Lua 5.1

The following code overwrites setmetatable to make it to work with __gc metamethod, not attaching anything to the original table. It will make __gc work exactly as Lua 5.2+, as you will see in the following examples.

-- Support to __gc metamethod
do
  local gcProxies = { }

  -- The values in gcProxies are strong because they refer to their own keys.
  -- So, it needs to be forced to have also weak values, since we want to remove each entry from gcProxies if its key (metatable) is not in use anymore anywhere.
  setmetatable(gcProxies, { __mode = 'kv' }) -- Make keys and values weak

  local _setmetatable = setmetatable
  function setmetatable(table, metatable)
    if metatable.__gc then
      -- Create an empty userdata (the only values in Lua 5.1 that work with __gc metamethod is userdata).
      -- Then, we insert it in gcProxies (weak table); so when metatable is not in use anymore, it will also remove it from gcProxies.
      gcProxies[metatable] = newproxy(true)

      -- __gc from metatable of gcProxies[metatable] call __gc from metatable
      getmetatable(gcProxies[metatable]).__gc = function()
        if type(metatable.__gc) == 'function' then
          metatable.__gc(table)
        end
      end
    end

    return _setmetatable(table, metatable)
  end
end

Example 1:

  local o = { x = 'hi' }
  setmetatable(o, { __gc = function(o) print(o.x) end })
  o = nil

  collectgarbage() --> hi

Example 2:

  local o = { x = 'hi' }
  local mt = { __gc = true }

  setmetatable(o, mt)
  mt.__gc = function(o) print(o.x) end
  o = nil

  collectgarbage() --> hi

Example 3:

  local mt = { __gc = function(o) print(o[1]) end }
  local list = nil

  for i = 1, 3 do
    list = setmetatable({ i, link = list }, mt)
  end
  list = nil

  collectgarbage() --> 3; 2; 1

Example 4:

  local A = { x = 'This is A' }
  local B = { f = A }

  setmetatable(B, { __gc = function(o) print(o.f.x) end })
  A, B = nil

  collectgarbage() --> This is A

Example 5:

do
  local t = {
    __gc = function()
      -- Your 'atexit' code comes here
      print('Finishing Lua program')
    end
  }
  setmetatable(t, t)

  _G['*ATEXIT*'] = t
end

--> Finishing Lua program

Example 6:

do
  local mt = {
    __gc = function(o)
      -- Executes on collectgarbage
      print('New cycle')

      setmetatable({ }, getmetatable(o)) -- Creates new object for next cycle
    end
  }

  -- Creates first object
  setmetatable({ }, mt) --> New cycle
end

collectgarbage() --> New cycle
collectgarbage() --> New cycle
collectgarbage() --> New cycle