clua

How to do clean up in generic for iterator function when breaking out of it


I'm implementing a generic for iterator function in C. The function that returns the iterator function for the generic for loop first allocates state information for the iterator function using lua_newuserdata(), like this:

struct mystate *s = (struct mystate *) lua_newuserdata(L, sizeof(struct mystate));
s->firstcall = 1;

Then I push the pointer as an upvalue to my C closure, like this:

lua_pushvalue(L, lua_gettop(L));
lua_pushcclosure(L, iteratorfunction, 1);

My iterator function then retrieves the pointer from the first upvalue and does some allocations on it, like this:

static int iteratorfunction(lua_State *L)
{
    struct mystate *s = (struct mystate *) lua_touserdata(L, lua_upvalueindex(1));

    if(s->firstcall) {
       s->file = fopen(...);
       s->data = malloc(...);
       ...
       s->firstcall = 0;
    }

    ...
}

Now my question is this: How should I make sure that s->file and s->data are freed correctly in case the script uses break to exit the generic for loop before it has finished? In that case, my iteratorfunction can't do the clean up because it isn't called all the way through. Instead, the generic for loop exits before my iterator function has finished.

Precisely, how can I make sure that fclose() is called on s->file and free() is called on s->data in case the script uses break to exit my generic for loop before it has finished?

I've had a look at the Lua source and io.lines seems to use metatables and the garbage collector to ensure the file handle is closed but I don't really understand how this works. It looks quite complicated and I'm not sure if I should be doing this in a similar way or whether there is an easier solution for my case.

Note that I'm still on Lua 5.0 so any suggestions for solutions should keep that in mind. Thanks!


Solution

  • To answer my own question, I'm now using finalizers (the __gc metamethod) as suggested by Egor. In code this looks like this:

    First we need to create a metatable whose __gc we can use to do the cleaning up:

    #define PRIVATEHANDLE "PRIVATE*"
    
    luaL_newmetatable(L, PRIVATEHANDLE);
    lua_pushliteral(L, "__index");
    lua_pushvalue(L, -2);
    lua_rawset(L, -3);  
    
    lua_pushstring(L, "__gc");
    lua_pushcclosure(L, iteratorfunction_gc, 0);
    lua_settable(L, -3);
    
    lua_pop(h, 1);
    

    We then need to associate the user data with the metatable so that our __gc method is called once Lua decides to delete our user data, hence we do:

    struct mystate *s = (struct mystate *) lua_newuserdata(L, sizeof(struct mystate));
    memset(s, 0, sizeof(struct mystate));
    
    luaL_getmetatable(L, PRIVATEHANDLE);
    lua_setmetatable(L, -2);
    

    Finally, we need to implement iteratorfunction_gc to do the actual clean up. This can look like this:

    static int iteratorfunction_gc(lua_State *L)
    {
        struct mystate *s = (struct mystate *) luaL_checkudata(L, 1, FILEHANDLE);
    
        if(s->file) fclose(s->file);
        if(s->data) free(s->data);
    
        ...additional cleanup here...
    
        return 0;
    }
    

    Tested it and this does the job really fine. Problem solved. No idea why people were trying to close this question.