ThePhD / sol2

Sol3 (sol2 v3.0) - a C++ <-> Lua API wrapper with advanced features and top notch performance - is here, and it's great! Documentation:
http://sol2.rtfd.io/
MIT License
4.16k stars 504 forks source link

Crash when accessing members of nested structure #1373

Open matusfedorko opened 2 years ago

matusfedorko commented 2 years ago

Hello,

the snippet below roughly demonstrates what we have in the real code in our project. ComplexStructC is a structure bound to lua composed of other structure(s) (ComplexStructB, ComplexStructA) also bound to Lua. We return ComplexStructC as a result of a calculation from a function called by lua (named GetComplexStructC in this snippet).

The lua script however saves only part of the result and discards the rest. In this sample it stores csc.b to bb and clears csc.

Also notice the garbage collector calls in between script invocations.

What seems to happen is that lua deallocates the memory of csc (which is of type ComplexStructC), because there are no more valid references left pointing to it from Lua, but that also takes down with it every part of csc, because it is in the same chunk of memory, so bb basically becomes a dangling reference.

Does this sound plausible ? Are there any solutions/workarounds to this ?

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

#include <iostream>

struct ComplexStructA
{
    int a = 10;
};

struct ComplexStructB
{
    ComplexStructA a;
    int b = 20;
};

struct ComplexStructC
{
    ComplexStructB b;
    int c = 30;
};

static ComplexStructC GetComplexStructC(const int a, const int b, const int c)
{
    ComplexStructC csc;
    csc.b.a.a = a;
    csc.b.b = b;
    csc.c = c;
    return csc;
}

static ComplexStructC GetComplexStructC()
{
    return GetComplexStructC(10, 20, 30);
}

static const char* CallStatusToString(sol::call_status s)
{
#define CASE(lbl) case sol::call_status::lbl: return #lbl
    switch (s)
    {
        CASE(ok);
        CASE(yielded);
        CASE(runtime);
        CASE(memory);
        CASE(handler);
        CASE(gc);
        CASE(syntax);
        CASE(file);
    default: return "";
    }
#undef CASE
}

int main()
{
    sol::state lua;
    lua.open_libraries(sol::lib::base, sol::lib::package, sol::lib::coroutine, sol::lib::string, sol::lib::os, sol::lib::math, sol::lib::table, sol::lib::io, sol::lib::debug);

    lua.new_usertype<ComplexStructA>("ComplexStructA", sol::call_constructor, sol::constructors<ComplexStructA()>(), "a", &ComplexStructA::a);
    lua.new_usertype<ComplexStructB>("ComplexStructB", sol::call_constructor, sol::constructors<ComplexStructB()>(), "a", &ComplexStructB::a, "b", &ComplexStructB::b);
    lua.new_usertype<ComplexStructC>("ComplexStructC", sol::call_constructor, sol::constructors<ComplexStructC()>(), "b", &ComplexStructC::b, "c", &ComplexStructC::c);

    lua.set("GetComplexStructC", sol::overload(static_cast<ComplexStructC(*)(int, int, int)>(&GetComplexStructC), static_cast<ComplexStructC(*)()>(&GetComplexStructC)));

    for (int i = 0; i < 20; ++i)
    {
        lua["testNum"] = i + 1;

        {
            auto res = lua.safe_script(R"(

            print('==================== ' .. tostring(testNum) .. ' ====================')

            csc = GetComplexStructC()

            print('csc.b.a.a', csc.b.a.a)
            print('csc.b.b', csc.b.b)
            print('csc.c', csc.c)

            bb = csc.b
            bb.b = 100

            print('csc.b.a.a', csc.b.a.a)
            print('csc.b.b', csc.b.b)
            print('csc.c', csc.c)

            print('bb.a.a', bb.a.a)
            print('bb.b', bb.b)

            csc = nil

        )", sol::script_pass_on_error);

            if (!res.valid())
                std::cerr << "status='" << CallStatusToString(res.status()) << "', message='" << res.get<sol::error>().what() << "'";
        }

        lua.collect_garbage();
        lua.collect_garbage(); // as reading in manuals, this function must be called twice to destroy all garbage.

        {
            auto res = lua.safe_script(R"(
                        -- here bb points to undefined memory
            print('bb.a.a', bb.a.a)
            print('bb.b', bb.b)

        )", sol::script_pass_on_error);

            if (!res.valid())
                std::cerr << "status='" << CallStatusToString(res.status()) << "', message='" << res.get<sol::error>().what() << "'";
        }
    }

    return 0;
}

This sample can also be viewed and tested here: https://godbolt.org/z/Eb6zGaKjr