Advertisement
Rochet2

Lua safe implementation of single userdata and GC

May 29th, 2014
425
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 14.82 KB | None | 0 0
  1. /*
  2.     Title:  Safe lua object implementation with userdata GC and single userdata per object
  3.     Author: Rochet2 https://github.com/Rochet2
  4.     A part of Eluna LuaEngine project: https://github.com/ElunaLuaEngine/Eluna
  5.     Using C++ and lua 5.2
  6.  
  7.     Decided  to document this piece of work since I did not find this anywhere on the net : |
  8.  
  9.     They way this works is that lua has a table that has weak values.
  10.     This means that the value can be garbage collected even though referenced by the table.
  11.     The table will have a reference that we use to access it.
  12.  
  13.     We will use the table to store our userdata and access it from C++.
  14.     The key will be pointer as string, so you will be able to access it just by having the pointer and the table reference.
  15.  
  16.     When pushing object, we check if the table has the pointer and if the value is valid.
  17.     If so, we will return that value.
  18.     Otherwise we will create a new userdata and push it to the lua table and return it.
  19.     This means that there is only ONE userdata for an object.
  20.  
  21.     Since we will NOT use a reference to the userdata other than the table reference,
  22.     which is weak and not counted, the userdata can be garbage collected.
  23.     This means that the userdata will not dangle in memory for the whole existence of the actual object once pushed and can be freed if unused.
  24.  
  25.     Additionally we can remove the value / turn the value to nil in the table once the object is destroyed.
  26.     This way when we check the userdata, we can check if the object still existsm if not -> error
  27.     This is good practice so you wont run into invalid pointer accessing errors and if you do, you get an error safely.
  28.  
  29.     Pros:
  30.         One userdata per object at a time
  31.         Garbage collecting of unused userdata
  32.         Still allows using lua mem managed objects easily ( just a setting in the regiester )
  33.         Allows one to set up invalid pointer errors, requires adding code to destructors : /
  34.         Does not use hashmap on C++ side
  35.  
  36.     Cons:
  37.         May possibly be slow since when pushing and checking we always need to get the userdata table and the value from it
  38.         On the other hand an article claimed accessing a variable with a lua ref was fast, so this can be fast as well :)
  39. */
  40.  
  41. // HEADER:
  42.  
  43. // this is a global variable
  44. // this will hold the hidden table reference that /should/ always be valid
  45. int tableref = LUA_REFNIL;
  46. // This holds our lua state and is also a global var
  47. lua_State* L = NULL;
  48.  
  49. template<typename T>
  50. class ObjectTemplate
  51. {
  52. public:
  53.     // type name
  54.     static const char* tname;
  55.     // if manageMemory == true, lua will handle memory management on GC call
  56.     static bool manageMemory;
  57.  
  58.     // Called when an object should be converted to a string
  59.     static int tostringT(lua_State* L)
  60.     {
  61.         T* obj = check(L, 1, false); // get self, no error
  62.         if (obj)
  63.         {
  64.             // object exists, push typename and pointer in a string
  65.             lua_pushfstring(L, "%s: (%p)", tname, obj);
  66.             return 1;
  67.         }
  68.         // object does not exist or was invalid, replace object with nil and push it as string
  69.         // (this is a copy paste from to string for nil)
  70.         lua_pushnil(L);
  71.         lua_replace(L, 1);
  72.         luaL_tolstring(L, 1, NULL);
  73.         return 1;
  74.     }
  75.  
  76.     // this is not needed with this implementation :)
  77.     // The reason is that there is only one userdata for object, so it works by default
  78.     //// Checked when obj == obj is used
  79.     //static int equalT(lua_State* L)
  80.     //{
  81.     //    // Get objects
  82.     //    T* obj1 = check(L, 1);
  83.     //    T* obj2 = check(L, 2);
  84.     //    // Check that they point to same address and push result
  85.     //    lua_pushboolean(L, obj1 == obj2);
  86.     //    return 1;
  87.     //}
  88.  
  89.     // This function is called on garbage collection for an Object
  90.     static int gcT(lua_State* L)
  91.     {
  92.         // debug
  93.         printf("gcT %s!\n", tname);
  94.  
  95.         // Check if lua does not handle mem management, return
  96.         if (!manageMemory)
  97.             return 0;
  98.  
  99.         // Get object pointer and check that has correct metatable
  100.         T** ptrHold = static_cast<T**>(luaL_checkudata(L, -1, tname));
  101.         if (ptrHold) // should always be true, check anyways
  102.             delete *ptrHold; // is userdata, delete
  103.         return 0;
  104.     }
  105.  
  106.     // Registers a new lua object type
  107.     // name will be used as typename (metatable name and error msg name)
  108.     // If gc is true, lua will handle the memory management for object pushed
  109.     static void Register(lua_State* L, const char* name, bool gc = false)
  110.     {
  111.         // Should register a type only once !!
  112.         ASSERT(!tname);
  113.  
  114.         // set typename and memory management settings for this typename
  115.         tname = name;
  116.         manageMemory = gc;
  117.  
  118.         // method table
  119.         lua_newtable(L);
  120.         int methods = lua_gettop(L);
  121.  
  122.         // store method table in globals so that
  123.         // scripts can add functions in Lua
  124.         lua_pushvalue(L, methods);
  125.         lua_setglobal(L, tname);
  126.  
  127.         luaL_newmetatable(L, tname);
  128.         int metatable = lua_gettop(L);
  129.  
  130.         // hide metatable from user
  131.         lua_pushvalue(L, methods);
  132.         lua_setfield(L, metatable, "__metatable");
  133.  
  134.         // required to access methods IE obj:GetName()
  135.         lua_pushvalue(L, methods);
  136.         lua_setfield(L, metatable, "__index");
  137.  
  138.         // to string
  139.         lua_pushcfunction(L, tostringT);
  140.         lua_setfield(L, metatable, "__tostring");
  141.  
  142.         // garbage collector
  143.         lua_pushcfunction(L, gcT);
  144.         lua_setfield(L, metatable, "__gc");
  145.  
  146.         // equality check
  147.         //lua_pushcfunction(L, equalT);
  148.         //lua_setfield(L, metatable, "__eq");
  149.  
  150.         // special method to get the object type
  151.         lua_pushcfunction(L, typeT);
  152.         lua_setfield(L, methods, "GetObjectType");
  153.  
  154.         // set the metatable for the method table
  155.         lua_setmetatable(L, methods);
  156.  
  157.         // remove the method table from the stack
  158.         lua_remove(L, methods);
  159.     }
  160.  
  161.     // Adds a new method / function for the object type
  162.     static void SetMethod(lua_State* L, const char* funcname, lua_CFunction func)
  163.     {
  164.         if (!funcname || !func)
  165.             return;
  166.  
  167.         // Get metatable and check that it is valid
  168.         luaL_getmetatable(L, tname);
  169.         if (!lua_istable(L, -1))
  170.         {
  171.             const char* err = lua_pushfstring(L, "%s missing metatable", tname);
  172.             luaL_argerror(L, -2, err);
  173.             return;
  174.         }
  175.  
  176.         // Get the method table from the metatable and check that it is valid
  177.         lua_getfield(L, -1, "__metatable");
  178.         lua_remove(L, -2); // remove metatable as it is not needed anymore
  179.         if (!lua_istable(L, -1))
  180.         {
  181.             const char* err = lua_pushfstring(L, "%s missing method table from metatable", tname);
  182.             luaL_argerror(L, -2, err);
  183.             return;
  184.         }
  185.  
  186.         // add the function to the method table
  187.         lua_pushstring(L, funcname);
  188.         lua_pushcclosure(L, func, 0);
  189.         lua_settable(L, -3);
  190.  
  191.         // remove the method table from stack
  192.         lua_remove(L, -1);
  193.     }
  194.  
  195.     // pushes userdata with the given pointer inside
  196.     static int push(lua_State* L, T const* obj)
  197.     {
  198.         // NULL handling
  199.         if (!obj)
  200.         {
  201.             lua_pushnil(L);
  202.             return 1;
  203.         }
  204.  
  205.         // If an object is mem managed by lua, there will / should always be
  206.         // always be only one userdata for it anyways in our implementation.
  207.         if (!manageMemory)
  208.         {
  209.             // Get our table with userdatas, could have error checking
  210.             lua_rawgeti(L, LUA_REGISTRYINDEX, tableref);
  211.             // Try finding the userdata for this pointer (pointer as string is key)
  212.             lua_pushfstring(L, "%p", obj);
  213.             lua_gettable(L, -2);
  214.             // pop the table
  215.             lua_remove(L, -2);
  216.             // check that the userdata is valid, no error
  217.             if (!lua_isnoneornil(L, -1) && luaL_checkudata(L, -1, tname))
  218.             {
  219.                 // debug
  220.                 printf("Pushed OLD userdata %s!\n", tname);
  221.  
  222.                 // return valid userdata
  223.                 return 1;
  224.             }
  225.             // was not valid, remove the userdata from stack
  226.             lua_remove(L, -1);
  227.         }
  228.  
  229.         // Create new userdata
  230.         T const** ptrHold = (T const**)lua_newuserdata(L, sizeof(T const**));
  231.         if (!ptrHold)
  232.         {
  233.             // Errored.. not enough memory?
  234.             // remove the userdata value and push nil instead
  235.             lua_remove(L, -1);
  236.             lua_pushnil(L);
  237.             return 1;
  238.         }
  239.         *ptrHold = obj; // set the pointer inside the userdata
  240.  
  241.         // Get metatable for the userdata and check that it is valid
  242.         luaL_getmetatable(L, tname);
  243.         if (!lua_istable(L, -1))
  244.         {
  245.             // Errored.. no metatable for the type?
  246.             // remove the userdata value and the metatable and push nil instead
  247.             lua_pop(L, 2);
  248.             lua_remove(L, -1);
  249.             lua_pushnil(L);
  250.             return 1;
  251.         }
  252.         // Set the metatable
  253.         lua_setmetatable(L, -2);
  254.  
  255.         if (!manageMemory)
  256.         {
  257.             // Get hidden lua table, could have error checking
  258.             lua_rawgeti(L, LUA_REGISTRYINDEX, tableref);
  259.             // place userdata in it with the pointer string as key
  260.             lua_pushfstring(L, "%p", obj);
  261.             lua_pushvalue(L, -3);
  262.             lua_settable(L, -3);
  263.             // and pop the table
  264.             lua_remove(L, -1);
  265.         }
  266.  
  267.         // debug
  268.         printf("Pushed NEW userdata %s!\n", tname);
  269.         return 1;
  270.     }
  271.  
  272.     // check / get a pointer of this type at given narg / index
  273.     // error will control whether it should error if the object is nil or wrong type
  274.     static T* check(lua_State* L, int narg, bool error = true)
  275.     {
  276.         // debug
  277.         printf("Checking %s!\n", tname);
  278.  
  279.         // Get the userdata, error if error is true
  280.         T** ptrHold = static_cast<T**>(error ? luaL_checkudata(L, narg, tname) : lua_touserdata(L, narg));
  281.         // if error was false, this can be NULL, so check it and error if necessary
  282.         if (!ptrHold)
  283.         {
  284.             if (error)
  285.             {
  286.                 // print an error
  287.                 char buff[256];
  288.                 snprintf(buff, 256, "%s expected, got %s", tname, luaL_typename(L, narg));
  289.                 luaL_argerror(L, narg, buff);
  290.             }
  291.             return NULL;
  292.         }
  293.  
  294.         // Only memory managed objects need this in this implementation
  295.         // This is only useful if the table element is erased by the destructor of object
  296.         if (!manageMemory)
  297.         {
  298.             // Check pointer validity
  299.  
  300.             // Get userdata table
  301.             lua_rawgeti(L, LUA_REGISTRYINDEX, tableref);
  302.             // get the userdata from the table for our pointer
  303.             lua_pushfstring(L, "%p", *ptrHold);
  304.             lua_gettable(L, -2);
  305.             // remove the userdata table
  306.             lua_remove(L, -2);
  307.             // check validity and remove the userdata
  308.             // could use luaL_checkudata to error here, but our implementation will have objects that inherit other types.
  309.             // This means that it would error for wrong type even if it was correct
  310.             bool valid = lua_isuserdata(L, -1);
  311.             lua_remove(L, -1);
  312.             if (!valid)
  313.             {
  314.                 // Should display error message regardless as a dead pointer is no good!
  315.                 char buff[256];
  316.                 snprintf(buff, 256, "%s expected, got pointer to nonexisting object (%s). This should never happen", tname, luaL_typename(L, narg));
  317.                 if (error)
  318.                 {
  319.                     luaL_argerror(L, narg, buff);
  320.                 }
  321.                 else
  322.                 {
  323.                     // this is a silent error since error was false
  324.                     printf("%s\n", buff);
  325.                 }
  326.                 return NULL;
  327.             }
  328.         }
  329.  
  330.         // All checks were correct, return the pointer from the userdata
  331.         return *ptrHold;
  332.     }
  333.  
  334.     // Removes the object userdata from lua if exists
  335.     static void RemoveRef(lua_State* L, T* obj)
  336.     {
  337.         // debug
  338.         printf("RemoveRef %s!\n", tname);
  339.  
  340.         // Get hidden userdata table, could check errors
  341.         lua_rawgeti(L, LUA_REGISTRYINDEX, tableref);
  342.         // get the userdata for our object
  343.         lua_pushfstring(L, "%p", obj);
  344.         lua_gettable(L, -2);
  345.         // check that it was valid
  346.         if (luaL_checkudata(L, -1))
  347.         {
  348.             lua_pushfstring(L, "%p", obj);
  349.             lua_pushnil(L);
  350.             lua_settable(L, -4);
  351.         }
  352.         lua_pop(L, 2);
  353.     }
  354. };
  355.  
  356. // CPP:
  357.  
  358. template<typename T> const char* ObjectTemplate<T>::tname = NULL;
  359. template<typename T> bool ObjectTemplate<T>::manageMemory = false;
  360.  
  361. class TestObj
  362. {
  363. public:
  364.     TestObj()
  365.     {
  366.         var = 5;
  367.     }
  368.  
  369.     ~TestObj()
  370.     {
  371.         ObjectTemplate<TestObj>::RemoveRef(L, this);
  372.     }
  373.  
  374.     int var;
  375. };
  376.  
  377. static int ObjGetVar(lua_State* L)
  378. {
  379.     // get self ( will error if not valid )
  380.     TestObj* obj = ObjectTemplate<TestObj>::check(L, 1);
  381.     lua_pushnumber(L, obj->var);
  382.     return 1;
  383. }
  384.  
  385. void LuaMain()
  386. {
  387.     // create lua base (state and libs)
  388.     L = luaL_newstate();
  389.     luaL_openlibs(L);
  390.  
  391.     // Create table
  392.     lua_newtable(L);
  393.     // Create metatable
  394.     lua_newtable(L);
  395.     // set __mode to v for metatable, which means weak values
  396.     lua_pushstring(L, "v");
  397.     lua_setfield(L, -2, "__mode");
  398.     // set the metatable for the table, pops metatable
  399.     lua_setmetatable(L, -2);
  400.     // create lua reference to the table and save it to the global variable
  401.     tableref = luaL_ref(L, LUA_REGISTRYINDEX);
  402.  
  403.     // register a new type and add a method for it
  404.     ObjectTemplate<TestObj>::Register(L, "TestObj");
  405.     ObjectTemplate<TestObj>::SetMethod(L, "GetVar", ObjGetVar);
  406.  
  407.     // make a few tests..
  408.     // Create a new object of the type and push it
  409.     TestObj* obj1 = new TestObj();
  410.     ObjectTemplate<TestObj>::push(L, obj1);
  411.  
  412.     // test lua garbage collection on the object
  413.     // should collect I think
  414.     lua_gc(L, LUA_GCCOLLECT, 0);
  415.  
  416.     // push it twice this time to test that it pushes the old again on second time
  417.     ObjectTemplate<TestObj>::push(L, obj1);
  418.     ObjectTemplate<TestObj>::push(L, obj1);
  419.  
  420.     // check it, should work
  421.     TestObj* obj2 = ObjectTemplate<TestObj>::check(L, -1);
  422.     // delete it, now pointers are invalid :(
  423.     delete obj2;
  424.  
  425.     // check it again, this will result in an error in this example implementation,
  426.     // since the invalid pointer check is there :)
  427.     TestObj* obj3 = ObjectTemplate<TestObj>::check(L, -1);
  428. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement