Skip to main content
The cover image for "Extending sol3's implicit type conversion"

Extending sol3's implicit type conversion

Sidebar

Many APIs in my game push `Vector3`s to and from Lua. It's such a common operation, that most of my functions used to look like this: ```cpp sol::table add(sol::table tPos) { Vector3f pos = TableToPos(tPos); // do something return PosToTable(pos); } ``` One of the benefits of sol is that it is able to bind Lua arguments to C++ function arguments, converting types implicitly. Having to convert from a table to a vector ourselves is quite annoying. It would be much nicer to have sol do it for us. Luckily, sol allows you to customise how types are retrieved and pushed to Lua using [Customisation Points](https://sol2.readthedocs.io/en/latest/tutorial/customization.html). When trying to convert a type from Lua to C++, sol will call certain templated functions. We will be customisating sol's behaviour using a technique called template specialization, which allows us to specialise a specific instance of the templated functions and structs. By the end of this article, we'll be able to use `Vector3` directly when using sol, allowing the above code to be turned into this: ```cpp Vector3f add(Vector3f pos) { // do something return pos; } ```

Many APIs in my game push Vector3s to and from Lua. It’s such a common operation, that most of my functions used to look like this:

sol::table add(sol::table tPos) {
    Vector3f pos = TableToPos(tPos);

    // do something
    return PosToTable(pos);
}

One of the benefits of sol is that it is able to bind Lua arguments to C++ function arguments, converting types implicitly. Having to convert from a table to a vector ourselves is quite annoying. It would be much nicer to have sol do it for us. Luckily, sol allows you to customise how types are retrieved and pushed to Lua using Customisation Points.

When trying to convert a type from Lua to C++, sol will call certain templated functions. We will be customisating sol’s behaviour using a technique called template specialization, which allows us to specialise a specific instance of the templated functions and structs. By the end of this article, we’ll be able to use Vector3 directly when using sol, allowing the above code to be turned into this:

Vector3f add(Vector3f pos) {
    // do something

    return pos;
}

sol_lua_get #

namespace sol {

template <typename T>
inline Vector3<T> sol_lua_get(sol::types<Vector3<T>>, lua_State *L, int index,
        sol::stack::record &tracking) {
    int absoluteIndex = lua_absindex(L, index);

    sol::table table = sol::stack::get<sol::table>(L, absoluteIndex);
    T x = table["x"];
    T y = table["y"];
    T z = table["z"];

    tracking.use(1);

    return { x, y, z };
}

sol_lua_get is the function used to convert from Lua to C++.

The first argument of this function is a dummy argument used by the meta-programming to select the correct function to use. The tracking record argument is used to tell sol what you did in this function. In this particular case, we only read a single argument from the stack, so we call:

tracking.use(1);

The example in the sol tutorial uses two arguments, and so calls use(2).

Note that it’s important to do this specialisation inside the same namespace as the original templated function. This is something that’s missed inside the sol tutorial.

sol_lua_push #

template <typename T>
inline int sol_lua_push(sol::types<Vector3<T>>, lua_State *L, const Vector3<T> &pos) {
    lua_createtable(L, 0, 3);

    lua_getglobal(L, "Vector");
    lua_setmetatable(L, -2);

    sol::stack_table vec(L);
    vec["x"] = pos.x;
    vec["y"] = pos.y;
    vec["z"] = pos.z;

    return 1;
}

sol_lua_push is the function used to convert from C++ to Lua.

Notice how stack_table is used to modify the table created by lua_createtable. This code also sets the global Vector as a metatable on the table, this is useful if you have a Lua Vector class.

lua_type_of #

template <typename T>
struct lua_type_of<Vector3<T>>
        : std::integral_constant<sol::type, sol::type::table> {};

This is a type trait used to tell sol that the Lua type should be a table.

sol_lua_check #

template <typename Handler, typename T>
inline bool sol_lua_check(sol::types<Vector3<T>>, lua_State *L, int index,
        Handler &&handler, sol::stack::record &tracking) {
    int absoluteIndex = lua_absindex(L, index);
    if (!stack::check<sol::table>(L, absoluteIndex, handler)) {
        tracking.use(1);
        return false;
    }

    sol::stack::get_field(L, "x", absoluteIndex);
    bool x = sol::stack::check<float>(L, -1);

    sol::stack::get_field(L, "y", absoluteIndex);
    bool y = sol::stack::check<float>(L, -1);

    sol::stack::get_field(L, "z", absoluteIndex);
    bool z = sol::stack::check<float>(L, -1);

    sol::stack::pop_n(L, 3);

    tracking.use(1);
    return x && y && z;
}

sol_lua_check is the function used to determine whether a stack value is of the correct type, and can be converted. In this case, we check that it’s a table and it has the required fields.

Wait, how do I actually use this? #

It should all be defined in a header after you include sol.hpp.

I like to have a Lua.hpp header with the following content:

#pragma once

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>

#include <types/Vector3.hpp>

namespace sol {

// All the specialisations here:
//   sol_lua_get, sol_lua_push sol_lua_check, lua_type_of

}

Instead of including sol.hpp in other files, I include Lua.hpp. This makes sure that sol receives the same defines and the same specialisations each time.

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Comments

Leave comment

Shown publicly next to your comment. Leave blank to show as "Anonymous".
Optional, to notify you if rubenwardy replies. Not shown publicly.
Max 1800 characters. You may use plain text, HTML, or Markdown.