27
\$\begingroup\$

I wanted a good way to move objects back and forth between Lua and C++, and I didn't want to use anything like LuaBind or the other available libraries I could find, so I instead wrote this. It's designed to be similar to the normal Lua API, which has functions like lua_tostring or lua_tonumber, but I wanted to make it 'just work' with whatever type I fed it. Basically for any function that had a variant for different types, I added a templated version of it. e.g. luaW_to<Foo>

https://bitbucket.org/alexames/luawrapper

// API Summary:
//
// LuaWrapper is a library designed to help bridge the gab between Lua and
// C++. It is designed to be small (a single header file), simple, fast,
// and typesafe. It has no external dependencies, and does not need to be
// precompiled; the header can simply be dropped into a project and used
// immediately. It even supports class inheritance to a certain degree. Objects
// can be created in either Lua or C++, and passed back and forth.
//
// In Lua, the objects are userdata, but through tricky use of metatables, they
// can be treated almost identically to tables.
//
// The main functions of interest are the following:
//  luaW_is<T>
//  luaW_to<T>
//  luaW_check<T>
//  luaW_push<T>
//  luaW_register<T>
//  luaW_hold<T>
//  luaW_release<T>
//  luaW_clean<T>
//
// These functions allow you to manipulate arbitrary classes just like you
// would the primitive types (e.g. numbers or strings). When all references
// to a userdata removed, the userdata will be deleted. In some cases, this
// may not be what you want, such as cases where an object is created in Lua,
// then passed to C++ code which owns it from then on. In these cases, you can
// call luaW_release, which releases LuaWrapper's hold on the userdata. This
// prevents it from being deallocated when all references disappear. When this
// is called, you are now responsible for calling luaW_clean manually when you
// are done with the object. Conversely, if an object is created in C++, but
// would like to pass ownership over to Lua, luaW_hold may be used.
//
// Additionally, metamethods __ctor and __dtor are provided, and will run when
// objects are created or destroyed respectively. Objects can also declare a
// list of other tables that they extend, and they will inherit all functions
// from that class.

// Todo:
//  Ensure the LuaWrapper table does not collide with other tables
//  Determine if it is useful to be able to call the destructor on released uds
//  Add a way to transfer ownership of uds so dtor and cleanup is automatic
//  Add some sort of serialization

#ifndef LUA_WRAPPER_H_
#define LUA_WRAPPER_H_

#include <iostream>
extern "C"
{
    #include "lua.h"
    #include "lauxlib.h"
}

#define LUAW_BUILDER

#define luaW_getregistry(L, s) \
     lua_getfield(L, LUA_REGISTRYINDEX, s)

#define luaW_setregistry(L, s) \
     lua_setfield(L, LUA_REGISTRYINDEX, s)

#define LUAW_CTOR_KEY "__ctor"
#define LUAW_DTOR_KEY "__dtor"
#define LUAW_EXTENDS_KEY "__extends"
#define LUAW_STORAGE_KEY "__storage"
#define LUAW_COUNT_KEY "__counts"
#define LUAW_HOLDS_KEY "__holds"
#define LUAW_WRAPPER_KEY "LuaWrapper"

#if 0
// For Debugging
// Prints the current Lua stack, including the values for some types
template <typename T>
void luaW_printstack(lua_State* L)
{
    int stack = lua_gettop(L);
    for (int i = 1; i <= stack; i++)
    {
        std::cout << std::dec << i << ": " << lua_typename(L, lua_type(L, i));
        switch(lua_type(L, i))
        {
        case LUA_TBOOLEAN: std::cout << " " << lua_toboolean(L, i); break;
        case LUA_TSTRING: std::cout << " " << lua_tostring(L, i); break;
        case LUA_TNUMBER: std::cout << " " << std::dec << (uintptr_t)lua_tointeger(L, i) << " (0x" << std::hex << lua_tointeger(L, i) << ")"; break;
        default: std::cout << " " << std::hex << lua_topointer(L, i); break;
        }
        std::cout << std::endl;
    }
}
#define LUAW_TRACE() \
    printf("%s:%d:%s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__)
#else
#define LUAW_TRACE()
#endif

template <typename T>
T* luaW_defaultallocator()
{
    return new T();
}

template <typename T>
void luaW_defaultdeallocator(T* obj)
{
    delete obj;
}

// This class is used with luaW_register as an alternative to using the
// normal constructor. Sometimes it's just easier to fill in the fields
// of a struct than to file in all the arguments in luaW_register,
// especially if you just want to set the last one or two.
template <typename T>
struct LuaWrapperOptions
{
    LuaWrapperOptions(
        const luaL_reg* table = NULL, const luaL_reg* metatable = NULL, const char** extends = NULL, bool disablenew = false, T* (*allocator)() = luaW_defaultallocator<T>, void (*deallocator)(T*) = luaW_defaultdeallocator<T>)
        : table(table), metatable(metatable), extends(extends), disablenew(disablenew), allocator(allocator), deallocator(deallocator) { }

    const luaL_reg* table;
    const luaL_reg* metatable;
    const char** extends;
    bool disablenew;
    T* (*allocator)();
    void (*deallocator)(T*);
};

// This class cannot actually to be instantiated. It is used only hold the
// table name and other information.
template <typename T>
class LuaWrapper
{
public:
    static const char* classname;
    static T* (*allocator)();
    static void (*deallocator)(T*);
private:
    LuaWrapper();
};
template <typename T> const char* LuaWrapper<T>::classname;
template <typename T> T* (*LuaWrapper<T>::allocator)();
template <typename T> void (*LuaWrapper<T>::deallocator)(T*);

// [-0, +0, -]
//
// Analogous to lua_is(boolean|string|*)
//
// Returns 1 if the value at the given acceptable index is of type T (or if
// strict is false, convertable to type T) and 0 otherwise.
template <typename T>
bool luaW_is(lua_State *L, int index, bool strict = false)
{
    LUAW_TRACE();
    bool equal = false;
    if (lua_touserdata(L, index) && lua_getmetatable(L, index))
    {
        // ... ud ... udmt
        luaL_getmetatable(L, LuaWrapper<T>::classname); // ... ud ... udmt Tmt
        equal = lua_rawequal(L, -1, -2);
        if (!equal && !strict)
        {
            lua_getfield(L, -2, LUAW_EXTENDS_KEY); // ... ud ... udmt Tmt udmt.__extends
            for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1))
            {
                // ... ud ... udmt Tmt udmt.__extends k v
                equal = lua_rawequal(L, -1, -4);
                if (equal)
                {
                    lua_pop(L, 2); // ... ud ... udmt Tmt udmt.__extends
                    break;
                }
            }
            lua_pop(L, 1); // ... ud ... udmt Tmt
        }
        lua_pop(L, 2); // ... ud ...
    }
    return equal;
}

// [-0, +0, -]
//
// Analogous to lua_to(boolean|string|*)
//
// Converts the given acceptable index to a T*. That value must be of type T;
// otherwise, returns NULL.
template <typename T>
T* luaW_to(lua_State* L, int index)
{
    LUAW_TRACE();
    T* obj = NULL;
    if (luaW_is<T>(L, index))
    {
        obj = *(T**)lua_touserdata(L, index);
    }
    return obj;
}

// [-0, +0, -]
//
// Analogous to luaL_check(boolean|string|*)
//
// Checks whether the function argument at index is a T and returns this object
template <typename T>
T* luaW_check(lua_State* L, int index)
{
    LUAW_TRACE();
    T* obj = NULL;
    if (luaW_is<T>(L, index))
    {
        obj = *(T**)lua_touserdata(L, index);
    }
    else
    {
        luaL_typerror(L, index, LuaWrapper<T>::classname);
    }
    return obj;
}

// [-0, +1, -]
//
// Analogous to lua_push(boolean|string|*)
//
// Pushes a userdata of type T onto the stack. If this object already exists in
// the Lua environment, it will assign the existing store to it. Otherwise, a
// new store will be created for it.
template <typename T>
void luaW_push(lua_State* L, T* obj)
{
    LUAW_TRACE();
    T** ud = (T**)lua_newuserdata(L, sizeof(T*)); // ... obj
    *ud = obj;
    luaL_getmetatable(L, LuaWrapper<T>::classname); // ... obj mt
    lua_setmetatable(L, -2); // ... obj
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // ... obj LuaWrapper
    lua_getfield(L, -1, LUAW_COUNT_KEY); // ... obj LuaWrapper LuaWrapper.counts
    lua_pushlightuserdata(L, obj); // ... obj LuaWrapper LuaWrapper.counts lud
    lua_gettable(L, -2); // ... obj LuaWrapper LuaWrapper.counts count
    int count = lua_tointeger(L, -1);
    lua_pushlightuserdata(L, obj); // ... obj LuaWrapper LuaWrapper.counts count lud
    lua_pushinteger(L, count+1); // ... obj LuaWrapper LuaWrapper.counts count lud count+1
    lua_settable(L, -4); // ... obj LuaWrapper LuaWrapper.counts count
    lua_pop(L, 3); // ... obj
}

// Instructs LuaWrapper that it owns the userdata, and can manage its memory.
// When all references to the object are removed, Lua is free to garbage
// collect it and delete the object.
//
// Returns true if luaW_hold took hold of the object, and false if it was
// already held
template <typename T>
bool luaW_hold(lua_State* L, T* obj)
{
    LUAW_TRACE();
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // ... LuaWrapper

    lua_getfield(L, -1, LUAW_HOLDS_KEY); // ... LuaWrapper LuaWrapper.holds
    lua_pushlightuserdata(L, obj); // ... LuaWrapper LuaWrapper.holds lud
    lua_rawget(L, -2); // ... LuaWrapper LuaWrapper.holds hold
    bool held = lua_toboolean(L, -1);
    // If it's not held, hold it
    if (!held)
    {
        // Apply hold boolean
        lua_pop(L, 1); // ... LuaWrapper LuaWrapper.holds
        lua_pushlightuserdata(L, obj); // ... LuaWrapper LuaWrapper.holds lud
        lua_pushboolean(L, true); // ... LuaWrapper LuaWrapper.holds lud true
        lua_rawset(L, -3); // ... LuaWrapper LuaWrapper.holds

        // Check count, if there's at least one, add a storage table
        lua_pop(L, 1); // ... LuaWrapper
        lua_getfield(L, -1, LUAW_COUNT_KEY); // ... LuaWrapper LuaWrapper.counts
        lua_pushlightuserdata(L, obj); // ... LuaWrapper LuaWrapper.counts lud
        lua_rawget(L, -2); // ... LuaWrapper LuaWrapper.counts count
        if (lua_tointeger(L, -1) > 0)
        {
            // Add the storage table if there isn't one already
            lua_pop(L, 2);
            lua_getfield(L, -1, LUAW_STORAGE_KEY); // ... LuaWrapper LuaWrapper.storage
            lua_pushlightuserdata(L, obj); // ... LuaWrapper LuaWrapper.storage lud
            lua_rawget(L, -2); // ... LuaWrapper LuaWrapper.storage store
            if (lua_isnoneornil(L, -1))
            {
                lua_pop(L, 1); // ... LuaWrapper LuaWrapper.storage
                lua_pushlightuserdata(L, obj); // ... LuaWrapper LuaWrapper.storage lud
                lua_newtable(L); // ... LuaWrapper LuaWrapper.storage lud store
                lua_rawset(L, -3); // ... LuaWrapper LuaWrapper.storage
                lua_pop(L, 2); // ...
            }
        }
        return true;
    }
    lua_pop(L, 3); // ...
    return false;
}

// Releases LuaWrapper's hold on an object. This allows the user to remove
// all references to an object in Lua and ensure that Lua will not attempt to
// garbage collect it.
template <typename T>
void luaW_release(lua_State* L, T* obj)
{
    LUAW_TRACE();
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // ... LuaWrapper
    lua_getfield(L, -1, LUAW_HOLDS_KEY); // ... LuaWrapper LuaWrapper.holds
    lua_pushlightuserdata(L, obj); // ... LuaWrapper LuaWrapper.holds lud
    lua_pushnil(L); // ... LuaWrapper LuaWrapper.counts lud nil
    lua_settable(L, -3); // ... LuaWrapper LuaWrapper.counts count
    lua_pop(L, 1); // ... LuaWrapper
}

// When luaW_clean is called on an object, values stored on it's Lua store
// become no longer accessible.
template <typename T>
void luaW_clean(lua_State* L, T* obj)
{
    LUAW_TRACE();
    lua_getfield(L, -1, LUAW_STORAGE_KEY); // ... LuaWrapper LuaWrapper.storage
    lua_pushlightuserdata(L, obj); // ... LuaWrapper LuaWrapper.storage lud
    lua_pushnil(L); // ... LuaWrapper LuaWrapper.storage lud nil
    lua_settable(L, -3);  // ... LuaWrapper LuaWrapper.store
    lua_pop(L, 2); // ...
}

// This function is called from Lua, not C++
//
// Calls the lua defined constructor ("__ctor") on a userdata. Assumes the
// userdata is on top of the stack, and numargs arguments are below it. This
// runs the CTOR_KEY function on T's metatable, using the object as the first
// argument and whatever else is below it as the rest of the arguments
template <typename T>
void luaW_constructor(lua_State* L, int numargs)
{
    LUAW_TRACE();
    // ... ud
    lua_getfield(L, -1, LUAW_CTOR_KEY); // ... ud ud.__ctor
    if (lua_type(L, -1) == LUA_TFUNCTION)
    {
        lua_pushvalue(L, -2); // ... ud ud.__ctor ud
        lua_insert(L, 1); // ud ... ud ud.__ctor
        lua_insert(L, 2); // ud ud.__ctor ... ud
        lua_insert(L, 3); // ud ud.__ctor ud  ...
        lua_call(L, numargs+1, 0); // ud
    }
    else
    {
        lua_pop(L, 1); // ... ud
    }
}

// This function is generally called from Lua, not C++
//
// Creates an object of type T and calls the constructor on it with the values
// on the stack as arguments to it's constructor
template <typename T>
int luaW_new(lua_State* L)
{
    LUAW_TRACE();
    int numargs = lua_gettop(L);
    T* obj = LuaWrapper<T>::allocator();
    luaW_push<T>(L, obj);
    luaW_hold<T>(L, obj);
    luaW_constructor<T>(L, numargs);
    return 1;
}

#ifdef LUAW_BUILDER

// This function is called from Lua, not C++
//
// This is an alternative way to construct objects. Instead of using new and a
// constructor, you can use a builder instead. A builder is called like this:
//
// f = Foo.build
// {
//     X = 10;
//     Y = 20;
// }
//
// This will then create a new Foo object, and then call f:X(10) and f:Y(20) on
// that object. The constructor is not called at any point. The keys in this
// table are used as function names on the metatable.
//
// This is sort of experimental, just to see if it ends up being useful.
template <typename T>
void luaW_builder(lua_State* L)
{
    LUAW_TRACE();
    if (lua_type(L, 1) == LUA_TTABLE)
    {
        // {} ud
        for (lua_pushnil(L); lua_next(L, 1); lua_pop(L, 1))
        {
            // {} ud k v
            lua_pushvalue(L, -2); // {} ud k v k
            lua_gettable(L, -4); // {} ud k v ud[k]
            lua_pushvalue(L, -4); // {} ud k v ud[k] ud
            lua_pushvalue(L, -3); // {} ud k v ud[k] ud v
            lua_call(L, 2, 0); // {} ud k v
        }
        // {} ud
    }
}

// This function is generally called from Lua, not C++
//
// Creates an object of type T and initializes it using its builder to
// initialize it
template <typename T>
int luaW_build(lua_State* L)
{
    LUAW_TRACE();
    T* obj = LuaWrapper<T>::allocator();
    luaW_push<T>(L, obj);
    luaW_hold<T>(L, obj);
    luaW_builder<T>(L);
    return 1;
}

#endif

// This function is called from Lua, not C++
//
// The default metamethod to call when indexing into lua userdata representing
// an object of type T. This will fisrt check the userdata's environment table
// and if it's not found there it will check the metatable. This is done so
// individual userdata can be treated as a table, and can hold thier own
// values.
template <typename T>
int luaW__index(lua_State* L)
{
    LUAW_TRACE();
    // obj key
    T* obj = luaW_to<T>(L, 1);
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // obj key LuaWrapper
    lua_getfield(L, -1, LUAW_STORAGE_KEY); // obj key LuaWrapper LuaWrapper.storage
    lua_pushlightuserdata(L, obj); // obj key LuaWrapper LuaWrapper.table lud
    lua_rawget(L, -2); // obj key LuaWrapper LuaWrapper.table table
    if (!lua_isnoneornil(L, -1))
    {
        lua_pushvalue(L, -4); // obj key LuaWrapper LuaWrapper.table table key
        lua_rawget(L, -2); // obj key LuaWrapper LuaWrapper.table table table[k]
        if (lua_isnoneornil(L, -1))
        {
            lua_pop(L, 4); // obj key
            lua_getmetatable(L, -2); // obj key mt
            lua_pushvalue(L, -2); // obj key mt k
            lua_rawget(L, -2); // obj key mt mt[k]
        }
    }
    else
    {
        lua_pop(L, 3); // obj key
        lua_getmetatable(L, -2); // obj key mt
        lua_pushvalue(L, -2); // obj key mt k
        lua_rawget(L, -2); // obj key mt mt[k]
    }
    return 1;
}

// This function is called from Lua, not C++
//
// The default metamethod to call when createing a new index on lua userdata
// representing an object of type T. This will index into the the userdata's
// environment table that it keeps for personal storage. This is done so
// individual userdata can be treated as a table, and can hold thier own
// values.
template <typename T>
int luaW__newindex(lua_State* L)
{
    LUAW_TRACE();
    // obj key value
    T* obj = luaW_to<T>(L, 1);
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // obj key value LuaWrapper
    lua_getfield(L, -1, LUAW_STORAGE_KEY); // obj key value LuaWrapper LuaWrapper.storage
    lua_pushlightuserdata(L, obj); // obj key value LuaWrapper LuaWrapper.storage lud
    lua_rawget(L, -2); // obj key value LuaWrapper LuaWrapper.storage store
    if (!lua_isnoneornil(L, -1))
    {
        lua_pushvalue(L, -5); // obj key value LuaWrapper LuaWrapper.storage store key
        lua_pushvalue(L, -5); // obj key value LuaWrapper LuaWrapper.storage store key value
        lua_rawset(L, -3); // obj key value LuaWrapper LuaWrapper.storage store
    }
    return 0;
}

// This function is called from Lua, not C++
//
// The __gc metamethod handles cleaning up userdata. The userdata's reference
// count is decremented, and if this is the final reference to the userdata,
// the __dtor metamethod is called, its environment table is nil'd and pointer
// deleted.
template <typename T>
int luaW__gc(lua_State* L)
{
    LUAW_TRACE();
    // obj
    T* obj = luaW_to<T>(L, 1);
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // obj LuaWrapper
    lua_getfield(L, -1, LUAW_COUNT_KEY); // obj LuaWrapper LuaWrapper.counts
    lua_pushlightuserdata(L, obj); // obj LuaWrapper LuaWrapper.counts lud
    lua_gettable(L, -2); // obj LuaWrapper LuaWrapper.counts count
    int count = lua_tointeger(L, -1);
    lua_pushlightuserdata(L, obj); // obj LuaWrapper LuaWrapper.counts count lud
    lua_pushinteger(L, count-1); // obj LuaWrapper LuaWrapper.counts count lud count-1
    lua_settable(L, -4); // obj LuaWrapper LuaWrapper.counts count
    lua_pop(L, 3); // obj LuaWrapper

    if (obj && 1 == count)
    {
        lua_getfield(L, -1, LUAW_DTOR_KEY); // obj obj.__dtor
        if (lua_type(L, -1) == LUA_TFUNCTION)
        {
            lua_pushvalue(L, -2); //  obj obj.__ctor obj
            lua_call(L, 1, 0); // obj
        }
        else
        {
            lua_pop(L, 1); // obj
        }
        luaW_release<T>(L, obj);
        luaW_clean<T>(L, obj);
        delete obj;
    }
    return 0;
}

// Run this to create a table and metatable for your class. You must have a
// correctly initialized LuaWrapper<T> for your class in order for this to
// properly initilize your class wrapper. This function will also take care of
// extending any classes T inherits from by copying the values in the metatable
// of the extended class to T's metatable (assuming T's metatable doesn't have
// something in that key already).
template <typename T>
void luaW_register(lua_State* L, const char* classname, const luaL_reg* table, const luaL_reg* metatable, const char** extends = NULL, bool disablenew = false, T* (*allocator)() = luaW_defaultallocator<T>, void (*deallocator)(T*) = luaW_defaultdeallocator<T>)
{
    LUAW_TRACE();
    LuaWrapper<T>::classname = classname;
    LuaWrapper<T>::allocator = allocator;
    LuaWrapper<T>::deallocator = deallocator;
    const luaL_reg defaulttable[] =
    {
        { "new", luaW_new<T> },
#ifdef LUAW_BUILDER
        { "build", luaW_build<T> },
#endif
        { NULL, NULL }
    };
    const luaL_reg defaultmetatable[] = { { "__index", luaW__index<T> }, { "__newindex", luaW__newindex<T> }, { "__gc", luaW__gc<T> }, { NULL, NULL } };
    const luaL_reg emptytable[] = { { NULL, NULL } };

    table = table ? table : emptytable;
    metatable = metatable ? metatable : emptytable;

    // Ensure that the LuaWrapper table is set up
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // LuaWrapper
    if (lua_isnil(L, -1))
    {
        lua_newtable(L); // nil {}
        luaW_setregistry(L, LUAW_WRAPPER_KEY); // nil
        luaW_getregistry(L, LUAW_WRAPPER_KEY); // nil LuaWrapper
        lua_newtable(L); // nil LuaWrapper {}
        lua_setfield(L, -2, LUAW_COUNT_KEY); // nil LuaWrapper
        lua_newtable(L); // nil LuaWrapper {}
        lua_setfield(L, -2, LUAW_STORAGE_KEY); // nil LuaWrapper
        lua_newtable(L); // nil LuaWrapper {}
        lua_setfield(L, -2, LUAW_HOLDS_KEY); // nil LuaWrapper
        lua_pop(L, 1); // nil
    }
    lua_pop(L, 1); //

    // Open table
    if (!disablenew)
    {
        luaL_register(L, LuaWrapper<T>::classname, defaulttable); // T
        luaL_register(L, NULL, table); // T
    }
    else
    {
        luaL_register(L, LuaWrapper<T>::classname, table); // T
    }

    // Open metatable, set up extends table
    luaL_newmetatable(L, LuaWrapper<T>::classname); // T mt
    lua_newtable(L); // T mt {}
    lua_setfield(L, -2, LUAW_EXTENDS_KEY); // T mt
    luaL_register(L, NULL, defaultmetatable); // T mt
    luaL_register(L, NULL, metatable); // T mt

    // Copy key/value pairs from extended metatables
    for (const char** e = extends; e && *e; ++e)
    {
        luaL_getmetatable(L, *e); // T mt emt
        if(lua_isnoneornil(L, -1))
        {
            lua_pop(L, 1); // T mt
            std::cout << "Error: did not open table " << *e << " before " << LuaWrapper<T>::classname << std::endl;
            continue;
        }
        lua_getfield(L, -2, LUAW_EXTENDS_KEY); // T mt emt mt.__extends
        lua_pushvalue(L, -2); // T mt emt mt.__extends emt
        lua_setfield(L, -2, *e); // T mt emt mt.__extends
        lua_getfield(L, -2, LUAW_EXTENDS_KEY); // T mt emt mt.__extends emt.__extends

        for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1))
        {
            // T mt emt mt.__extends emt.__extends k v
            lua_pushvalue(L, -2); // T mt emt mt.__extends emt.__extends k v k
            lua_pushvalue(L, -2); // T mt emt mt.__extends emt.__extends k v k
            lua_rawset(L, -6); // T mt emt mt.__extends emt.__extends k v
        }

        lua_pop(L, 2); // T mt emt

        for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1))
        {
            // T mt emt k v
            lua_pushvalue(L, -2); // T mt emt k v k
            lua_gettable(L, -5); // T mt emt k v mt[k]
            if(lua_isnoneornil(L, -1))
            {
                lua_pop(L, 1); // T mt emt k v
                lua_pushvalue(L, -2); // T mt emt k v k
                lua_pushvalue(L, -2); // T mt emt k v k v
                lua_rawset(L, -6); // T mt emt k v
            }
            else
            {
                lua_pop(L, 1); // T mt k v
            }
        }
        lua_pop(L, 1); // T mt
    }
    lua_setmetatable(L, -2); // T
    lua_pop(L, 1); //
}

// Same as above, except sometimes its nice to be able to only have to set the
// fields you care about using a struct.
template<typename T>
void luaW_register(lua_State* L, const char* classname, LuaWrapperOptions<T>& options)
{
    luaW_register(L, classname, options.table, options.metatable, options.extends, options.disablenew, options.allocator, options.deallocator);
}

#undef luaW_getregistry
#undef luaW_setregistry

#endif // LUA_WRAPPER_H_
\$\endgroup\$
6
  • \$\begingroup\$ Can you elaborate on why this is better than Luabind? \$\endgroup\$ Commented Jul 17, 2011 at 19:01
  • 2
    \$\begingroup\$ I don't know if it's better or worse, just different. This aims for simplicity first. LuaBind practically makes up it's own language-within-a-language with it's use of metaprogramming and the [] operator. This is a single header and does not depend on boost or any external library other than iostream (and even that can probably be done away with). It also fits in naturally with the rest of the Lua functions that operate on types so if you know how to use those this is basically the same. I haven't used LuaBind much, but I suspect this requires a little more heavy lifting on your part. \$\endgroup\$
    – Alex Ames
    Commented Jul 18, 2011 at 0:57
  • \$\begingroup\$ LuaBridge doesn't have dependencies and does what you want to do. To print the stack you could also use this. Both designed for simplicity. In particular, you write "It is now the programmer's responsibility to run luaW_clean". This doesn't sound like simplicity and safety of use. \$\endgroup\$ Commented Dec 7, 2012 at 7:54
  • 1
    \$\begingroup\$ @DmitryLedentsov: This code is over a year old at this point and that requirement has been done away with since then (though I think the documentation might need to be updated to reflect this). LuaBridge looks nice though. Still, I'm happy with LuaWrapper for my projects for the time being. \$\endgroup\$
    – Alex Ames
    Commented Dec 7, 2012 at 18:54
  • \$\begingroup\$ What is ud ... udmt Tmt ? \$\endgroup\$
    – JaDogg
    Commented Jul 5, 2015 at 16:38

2 Answers 2

8
+100
\$\begingroup\$

First thing I would do is standardize indents, from this:

void luaW_printstack(lua_State* L)
{
    int stack = lua_gettop(L);
    for (int i = 1; i <= stack; i++)
    {
        std::cout << std::dec << i << ": " << lua_typename(L, lua_type(L, i));
        switch(lua_type(L, i))
        {
        case LUA_TBOOLEAN: std::cout << " " << lua_toboolean(L, i); break;
        case LUA_TSTRING: std::cout << " " << lua_tostring(L, i); break;
        case LUA_TNUMBER: std::cout << " " << std::dec << (uintptr_t)lua_tointeger(L, i) << " (0x" << std::hex << lua_tointeger(L, i) << ")"; break;
        default: std::cout << " " << std::hex << lua_topointer(L, i); break;
        }
        std::cout << std::endl;
    }
}

To this:

void luaW_printstack(lua_State* L)
{
    int stack = lua_gettop(L);
    for (int i = 1; i <= stack; i++)
    {
        std::cout << std::dec << i << ": " << lua_typename(L, lua_type(L, i));
        switch(lua_type(L, i))
        {
            case LUA_TBOOLEAN: std::cout << " " << lua_toboolean(L, i); break;
            case LUA_TSTRING: std::cout << " " << lua_tostring(L, i); break;
            case LUA_TNUMBER: std::cout << " " << std::dec << (uintptr_t)lua_tointeger(L, i) << " (0x" << std::hex << lua_tointeger(L, i) << ")"; break;
            default: std::cout << " " << std::hex << lua_topointer(L, i); break;
        }
        std::cout << std::endl;
    }
}

In my opinion this makes it much easier to follow. This allows the reader to quickly skim and find the hierarchy of the block they are in much quicker. Generally, when working in languages like the C/C99/C++/C# languages, Python, PHP, Perl, Visual Basic, etc., we assume each line is a statement. (Or no more than a couple.) When many statements are combined on a single line, we start to make mistakes and our assumptions can lead us astray.

Similarly, though less critical, the case statements are generally most readable when broken across lines, though that is up to you. I mean like this would be more readable:

switch(lua_type(L, i))
{
    case LUA_TBOOLEAN:
        std::cout << " " << lua_toboolean(L, i);
        break;
    case LUA_TSTRING: 
        std::cout << " " << lua_tostring(L, i); 
        break;
    case LUA_TNUMBER: 
        std::cout << " " << std::dec << (uintptr_t)lua_tointeger(L, i) << " (0x" << std::hex << lua_tointeger(L, i) << ")"; 
        break;
    default:
        std::cout << " " << std::hex << lua_topointer(L, i);
        break;
}

It allows the reader (and likely programmer) to easily distinguish block-from-block, and thus, each section of code from each other section of code.


Pre-processor if statements are very powerful, so when I see something like this:

#if 0
// For Debugging
// Prints the current Lua stack, including the values for some types
template <typename T>

It makes me wonder about the programmers intent? Perhaps, instead, you should define a:

#define LUA_DEBUG

#if LUA_DEBUG

Which you can then comment out the #define line to change the effect of it. You could reuse this later, of course, to make things much easier to deal with. Later on.


These two long lines in this constructor:

struct LuaWrapperOptions
{
    LuaWrapperOptions(
        const luaL_reg* table = NULL, const luaL_reg* metatable = NULL, const char** extends = NULL, bool disablenew = false, T* (*allocator)() = luaW_defaultallocator<T>, void (*deallocator)(T*) = luaW_defaultdeallocator<T>)
        : table(table), metatable(metatable), extends(extends), disablenew(disablenew), allocator(allocator), deallocator(deallocator) { }

    const luaL_reg* table;
    const luaL_reg* metatable;
    const char** extends;
    bool disablenew;
    T* (*allocator)();
    void (*deallocator)(T*);
};

Are very hard to read, though not on purpose. All those inline default values tend to make it hard to read. (You can't even tell how many there are without going on some deep comma-searching.) Cleaning that up makes it far clearer to follow in the future. Even a small change such as the following helps:

struct LuaWrapperOptions
{
    LuaWrapperOptions(
        const luaL_reg* table = NULL,
        const luaL_reg* metatable = NULL,
        const char** extends = NULL,
        bool disablenew = false,
        T* (*allocator)() = luaW_defaultallocator<T>,
        void (*deallocator)(T*) = luaW_defaultdeallocator<T>)
        : table(table), metatable(metatable), extends(extends), disablenew(disablenew), allocator(allocator), deallocator(deallocator) { }

    const luaL_reg* table;
    const luaL_reg* metatable;
    const char** extends;
    bool disablenew;
    T* (*allocator)();
    void (*deallocator)(T*);
};

This allows the reader to easily tell that there are just a few parameters there. Though, it could be improved much more, by breaking things more verbosely, as so:

struct LuaWrapperOptions
{
    LuaWrapperOptions(
            const luaL_reg* table = NULL,
            const luaL_reg* metatable = NULL,
            const char** extends = NULL,
            bool disablenew = false,
            T* (*allocator)() = luaW_defaultallocator<T>,
            void (*deallocator)(T*) = luaW_defaultdeallocator<T>
        )
        : table(table),
            metatable(metatable),
            extends(extends),
            disablenew(disablenew),
            allocator(allocator),
            deallocator(deallocator)
    { }

    const luaL_reg* table;
    const luaL_reg* metatable;
    const char** extends;
    bool disablenew;
    T* (*allocator)();
    void (*deallocator)(T*);
};

Part of the issue with the original line is that with it being extremely long, parts of the code would be hidden even on large screens. Generally, code is easiest to read top-to-bottom, and creating a lot of noise on the side like that disrupts the flow of your eyes. Another issue is that you cannot visibly distinguish bugs on the sides of the code like that, and can often forget about the excess of code that you may be unable to see in the regular viewport.


I would also separate the outer-most blocks of code from other code. I.e.:

int luaW__newindex(lua_State* L)
{
    LUAW_TRACE();
    // obj key value
    T* obj = luaW_to<T>(L, 1);
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // obj key value LuaWrapper
    lua_getfield(L, -1, LUAW_STORAGE_KEY); // obj key value LuaWrapper LuaWrapper.storage
    lua_pushlightuserdata(L, obj); // obj key value LuaWrapper LuaWrapper.storage lud
    lua_rawget(L, -2); // obj key value LuaWrapper LuaWrapper.storage store
    if (!lua_isnoneornil(L, -1))
    {
        lua_pushvalue(L, -5); // obj key value LuaWrapper LuaWrapper.storage store key
        lua_pushvalue(L, -5); // obj key value LuaWrapper LuaWrapper.storage store key value
        lua_rawset(L, -3); // obj key value LuaWrapper LuaWrapper.storage store
    }
    return 0;
}

To:

int luaW__newindex(lua_State* L)
{
    LUAW_TRACE();
    // obj key value
    T* obj = luaW_to<T>(L, 1);
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // obj key value LuaWrapper
    lua_getfield(L, -1, LUAW_STORAGE_KEY); // obj key value LuaWrapper LuaWrapper.storage
    lua_pushlightuserdata(L, obj); // obj key value LuaWrapper LuaWrapper.storage lud
    lua_rawget(L, -2); // obj key value LuaWrapper LuaWrapper.storage store

    if (!lua_isnoneornil(L, -1))
    {
        lua_pushvalue(L, -5); // obj key value LuaWrapper LuaWrapper.storage store key
        lua_pushvalue(L, -5); // obj key value LuaWrapper LuaWrapper.storage store key value
        lua_rawset(L, -3); // obj key value LuaWrapper LuaWrapper.storage store
    }

    return 0;
}

Again, another personal taste, but it allows you to more easily distinguish blocks from each other. Putting code directly above an if-block like that can give it the impression that it is part of that if block.


Another note, as @Hosch250 said, hardcoded returns are generally frowned upon. If you absolutely require them, they should be indicative of a default value, or if they are an error, then an error value.

Your comments seem appropriate, you use them to keep track of things that the code does not necessarily tell you directly. Which is good. Bad comments tend to be those that overly explain the code.

I.e.:

int number = 10; // create an integer

Tends to be bad, whereas:

int number = 10; // we have 100 of them, and 10 groups to do it, so each group is 10 objects

Is generally a bit better. (Although if the code itself can explain the idea then there are no comments needed.) Your comments tend to be very helpful and explanatory. Even the shorthand ones are very helpful, and lead to reasons you do things the way you do.

(Merely examples - results may vary.)

Otherwise, these suggestions should be taken into consideration with the suggestions by @Hosch250. Overall, this is very well-written code.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ I'm not sure why this 4 year old post is suddenly getting attention :P The code in the repository is much newer than the code posted here. Regarding the function luaW_printstack: that function no longer exists. Nor does the debug code that was hidden in the #if 0. LuaWrapperOptions and its long constructor were also ditched. I disagree about the hardcoded return value: in this case it's necessary. The function is a callback that requires a specific signature where the return value is how many stack elements are to be returned to Lua, and the function mentioned always returns 0 elements to Lua. \$\endgroup\$
    – Alex Ames
    Commented Jul 14, 2015 at 16:41
  • 1
    \$\begingroup\$ @AlexAmes - this post was one of the highest-scoring unanswered questions on the site, and a bounty was offered for to "clear out" the unanswered queue. You can thank janos for the attention ;-). \$\endgroup\$
    – rolfl
    Commented Jul 14, 2015 at 17:27
5
\$\begingroup\$

#defines

This is not a good use of #define. This should be a const string instead:

#define LUAW_CTOR_KEY "__ctor"

And this should be a function:

#define LUAW_TRACE() \
    printf("%s:%d:%s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__)

Hardcoded returns:

template <typename T>
int luaW__newindex(lua_State* L)
{
    LUAW_TRACE();
    // obj key value
    T* obj = luaW_to<T>(L, 1);
    luaW_getregistry(L, LUAW_WRAPPER_KEY); // obj key value LuaWrapper
    lua_getfield(L, -1, LUAW_STORAGE_KEY); // obj key value LuaWrapper LuaWrapper.storage
    lua_pushlightuserdata(L, obj); // obj key value LuaWrapper LuaWrapper.storage lud
    lua_rawget(L, -2); // obj key value LuaWrapper LuaWrapper.storage store
    if (!lua_isnoneornil(L, -1))
    {
        lua_pushvalue(L, -5); // obj key value LuaWrapper LuaWrapper.storage store key
        lua_pushvalue(L, -5); // obj key value LuaWrapper LuaWrapper.storage store key value
        lua_rawset(L, -3); // obj key value LuaWrapper LuaWrapper.storage store
    }
    return 0;
}

I'm not sure why you hardcode a 0 return value there. Is it a default index? Based on it being the only return in the function, it almost looks like a signal of success. If so, this method could possibly return a Boolean or become void.

\$\endgroup\$
1
  • \$\begingroup\$ I'm not sure why this post is suddenly getting so much attention. Regarding LUAW_TRACE(), that can't be a function without requiring the user include the file, line and function as the arguments for every call. The whole point of the macro is the have the line numbers and function magically populated. However, in the current version of the code LUAW_TRACE has been removed anyway. Regarding the hardcoded return value: This is a Lua callback function. It needs to have have the function signature it does, and the return value is how many values off the top of the stack are being returned to Lua. \$\endgroup\$
    – Alex Ames
    Commented Jul 14, 2015 at 16:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.