6

I am attempting to create a GC finalizer for a function value by storing it in a weak table using the C API.

I started off by writing a prototype in pure Lua 5.2:

local function myfinalizer()
   print 'Called finalizer'
end

function myfunc()
   print 'Called myfunc'
end

local sentinels = setmetatable({}, { __mode='k' })
sentinels[myfunc] = setmetatable({}, { __gc=myfinalizer })

myfunc()
myfunc = nil
collectgarbage 'collect'

print 'Closing Lua'

Resulting output:

Called myfunc
Called finalizer
Closing Lua


The prototype seems to be working as intended. Below is the C version:

#include <stdlib.h>
#include <stdio.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

static int my_finalizer(lua_State *L)
{
    puts("Called finalizer");
    return 0;
}

static int my_func(lua_State *L)
{
    puts("Called myfunc");
    return 0;
}

int main(void)
{
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    // create sentinels table (weak keys) in registry
    lua_newtable(L);                                    // t
    lua_newtable(L);                                    // t mt
    lua_pushstring(L, "k");                             // t mt v
    lua_setfield(L, -2, "__mode");                      // t mt
    lua_setmetatable(L, -2);                            // t
    lua_setfield(L, LUA_REGISTRYINDEX, "sentinels");    //

    // push global function and register as sentinel
    lua_pushcfunction(L, my_func);                      // f
    lua_getfield(L, LUA_REGISTRYINDEX, "sentinels");    // f t
    lua_pushvalue(L, 1);                                // f t k
    lua_newuserdata(L, 0);                              // f t k v
    lua_newtable(L);                                    // f t k v mt
    lua_pushcfunction(L, my_finalizer);                 // f t k v mt v
    lua_setfield(L, -2, "__gc");                        // f t k v mt
    lua_setmetatable(L, -2);                            // f t k v
    lua_settable(L, -3);                                // f t
    lua_pop(L, 1);                                      // f
    lua_setglobal(L, "myfunc");                         //

    // execute test script and exit
    if (luaL_dostring(L, "myfunc(); myfunc=nil; collectgarbage'collect'")) {
        printf("Error: %s\n", lua_tostring(L, -1));
    }
    lua_gc(L, LUA_GCCOLLECT, 0);    // suggestion: two full gc cycles
    fflush(stdout);                 // suggestion: immediate flush
    puts("Closing Lua");
    lua_close(L);

    fflush(stdout);
    return EXIT_SUCCESS;
}

Compiled using:

$ gcc -std=c99 -Wall -Werror -pedantic -O2 -o main main.c -ldl -llua52 -lm

Resulting output:

Called myfunc
Closing Lua
Called finalizer

The C version has a few minor differences:

  1. Instead of a local sentinels table I am storing in the registry.
  2. Using a zero sized userdata instead of a table for sentinel value with __gc metamethod.

I am confused as to why in the C version the myfunc finalizer doesn't execute after running a full collection cycle. What am I doing wrong?

greatwolf
  • 20,287
  • 13
  • 71
  • 105
Adam
  • 3,053
  • 2
  • 26
  • 29
  • Try with a userdata proxy in the prototype code and see if you get the same behaviour? Try with a table proxy in the C code too? Try calling collect after you return from luaL_dostring? Does the result change if you use two collectgarbage calls? – Etan Reisner Jun 06 '14 at 18:35
  • @EtanReisner Thanks for the suggestions. I tried added `lua_gc(L, 0, LUA_GCCOLLECT)` after returning from `luaL_dostring` as well as replacing the userdata with a table, I get the same result. – Adam Jun 06 '14 at 18:39
  • Does moving your fflush to before the lua_close change the output ordering? Could this be an output flushing issue? Though I also recall there being a requirement for two full cycles in some cases but I don't see why using C would make that different. – Etan Reisner Jun 06 '14 at 19:16
  • @EtanReisner Doesn't seem to have any effect. I updated the posted C code per your suggestions. – Adam Jun 06 '14 at 19:24
  • @Adam One tiny observation - it's also reproducible with Lua5.1. And You should use `$ gcc -std=c99 -Wall -Werror -pedantic -O2 -o main main.c -llua52 -ldl -lm`, because with newer versions of GCC - order of linkage parameters matter. – Kamiccolo Jun 06 '14 at 20:00
  • @Kamiccolo Good to know, and thanks for verifying the behavior in Lua 5.1 – Adam Jun 06 '14 at 20:02

1 Answers1

5

As the Lua manual states:

Only objects that have an explicit construction are removed from weak tables. Values, such as numbers and light C functions, are not subject to garbage collection, and therefore are not removed from weak tables (unless its associated value is collected).

Your my_func is pushed without any upvalues, so it is a light C function, and it isn't removed from weak tables during garbage collection, so the associated userdata does not become garbage before you close the Lua state. Your code should work if you use a Lua function instead of my_func, or if you push my_func with upvalues (and if you fix the order of the arguments in the lua_gc call!).

To summarize, the following value types are not removed from weak tables (given that their associated keys/values aren't removed either):

  • booleans
  • numbers
  • strings
  • light userdata
  • light C functions (Lua 5.2 only)

As a consequence your program should work fine with Lua 5.1 because there are no light C functions (you still have to fix the lua_gc call).

siffiejoe
  • 4,141
  • 1
  • 16
  • 16
  • Made the function a closure and now works as expected, thanks for the great answer. – Adam Jun 07 '14 at 00:18