Weak table and GC finalizer using C API
Asked Answered
C

1

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?

Cryptogenic answered 6/6, 2014 at 17:51 Comment(6)
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?Metchnikoff
@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.Cryptogenic
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.Metchnikoff
@EtanReisner Doesn't seem to have any effect. I updated the posted C code per your suggestions.Cryptogenic
@Cryptogenic 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.Donaldson
@Donaldson Good to know, and thanks for verifying the behavior in Lua 5.1Cryptogenic
Z
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).

Zofiazoha answered 6/6, 2014 at 23:40 Comment(1)
Made the function a closure and now works as expected, thanks for the great answer.Cryptogenic

© 2022 - 2024 — McMap. All rights reserved.