Skip to content
This repository has been archived by the owner on Dec 4, 2020. It is now read-only.

Sol Lua Binding Upgrade

Zach Toogood edited this page Nov 25, 2020 · 4 revisions

Intro

If you're here, you're probably aware of how we use Lua (Portuguese for Moon, not an acronym) as a scripting language for quick iteration, and to lower the barrier to entry for newcomers. Aside from a couple of quirks that get under peoples' skin (1-indexed arrays); it is a wonderful language that is very fast to pick up and has surprising versatility.

In order to use Lua alongside our Core C++ code, we need to bind C++ functions to Lua, and represent C++ Types as Lua Usertypes. This is achieved by using a binding system, or binding library.

Our current binding system is called Lunar and was originally published as a code snippet around 2009. At the time, there probably wasn't much available in terms of binding libraries, so this would have been a godsend in terms of "easy" interop with C++. Unfortunately, Lunar hasn't been upgraded or iterated on... ever. We haven't touched it (apart from formatting passes) since Sep 16, 2011.

If it was rock-solid and easy to use/understand/extend, then we would be perfectly fine to leave it alone forever. This however is not the case. The binding system and the process of getting information into and out of the Lua state is fragile and arcane. Much developer time has been wasted "fiddling with the stack". Since our adoption of Lunar pre-2011, the Lua-binding ecosystem has moved on and there are a wealth of viable libraries allowing for safer usage, modern workflows, and easier to understand/maintain code.

The most promising and well supported library at the moment is sol (sol2 v3, referred to as Sol/sol for clarity).

Examples

Usage

#define SOL_ALL_SAFETIES_ON 1
#include "sol/sol.hpp"

Basic

int main()
{
    sol::state lua(); // Create lua state
    lua.open_libraries(); // Open all lua standard libs
  
    lua.script("print('Hello from Lua!')");

    return 0;
}

Lua from C++

int main()
{
    sol::state lua(); // Create lua state
    lua.open_libraries(); // Open all lua standard libs
  
    lua.script("function callMe(str) print(str) end");

    lua["callMe"]("Test String");

    return 0;
}

C++ from Lua

int main()
{
    sol::state lua(); // Create lua state
    lua.open_libraries(); // Open all lua standard libs
  
    auto nativeFunc = [](std::string str) { std::cout << str << "\n"; };
    
    lua["nativeFunc"] = nativeFunc;

    lua.script("nativeFunc('Hello from nativeFunc!')");

    return 0;
}

Bind free functions

namespace luautils
{
    void sayHello()
    {
        std::cout << "Hello!\n";
    }
}

int main()
{
    sol::state lua(); // Create lua state
    lua.open_libraries(); // Open all lua standard libs
  
    lua.set_function("sayHello", &luautils::sayHello);

    lua.script("sayHello()");

    return 0;
}

Simple Usertypes

struct Entity
{
    int ID;
    int getID()
    {
        return ID;
    }
};

int main()
{
    sol::state lua(); // Create lua state
    lua.open_libraries(); // Open all lua standard libs
  
    lua.new_usertype<Entity>("Entity",
        "getID()", &Entity::getID()
    );
    
    lua.script("function printEntityID(entity) print(entity:getID()) end");

    auto entity = Entity{42};

    lua["printEntityID"](entity);

    return 0;
}

Binding Wrappers for Usertypes

// Underlying Entity
struct Entity
{
    int ID;
    int Zone;

    int getID()
    {
        return ID;
    }

    int getZone()
    {
        return Zone;
    }
};

// "Wrapper" around underlying Entity
struct LuaEntity
{
    Entity* m_PEntity;

    int getID()
    {
        return m_PEntity->ID;
    }

    int getZone()
    {
        return m_PEntity->Zone;
    }

    int getLongID()
    {
        // Other logic can live here <--
        return otherutils::makeLongID(m_PEntity->ID, m_PEntity->Zone);
    }
};

int main()
{
    sol::state lua(); // Create lua state
    lua.open_libraries(); // Open all lua standard libs
  
    lua.new_usertype<LuaEntity>("LuaEntity",
        "getID()", &LuaEntity::getID(),
        "getZone()", &LuaEntity::getZone(),
        "getLongID()", &LuaEntity::getLongID()
    );

    lua.script("function printEntityInfo(entity) print(entity:getID()); print(entity:getZone()); print(entity:getLongID()); end");

    auto entity = Entity{42, 99};
    auto luaEntity = LuaEntity{entity}

    lua["printEntityInfo"](luaEntity);

    return 0;
}

Basic interop with Lunar

uint32 someExistingBinding(lua_State* L)
{
    sol::state_view lua(L); // Create non-owning view into existing lua state
  
    ... // Continue using 'lua' and the sol functionality...

    return 0;
}

Safety wrappers

template <typename... Args>
auto safeCall(sol::state& lua, std::string funcName, Args&&... args)
{
    auto result = lua[funcName](std::forward<Args>(args)...);
    if (!result.valid())
    {
        sol::error e = result;
        printf("lua err: %s\n", e.what());
    }
    return result;
}

int main()
{
    sol::state lua;
    lua.open_libraries();
    lua.script(R"(
        function test0()
            print(42)
        end

        function test1(num1)
            print(num1 * num1)
        end

        function test2(num1, num2)
            print(num1 * num2)
        end
    )");

    safeCall(lua, "test0");
    safeCall(lua, "test1", 2);
    
    if (!safeCall(lua, "test2", "bad args").valid())
    {
        printf("Bad times :(");
    }

    // Prints:
    // lua err: [string "..."]:11: attempt to mul a 'string' with a 'nil'
    // stack traceback:
    //         [C]: in metamethod 'mul'
    //         [string "..."]:11: in function 'test2'
    // Bad times :(

    return 0;
}
Clone this wiki locally