sol2/tests/run_time/source/gc.cpp
2022-06-25 04:00:53 -04:00

621 lines
13 KiB
C++

// sol2
// The MIT License (MIT)
// Copyright (c) 2013-2022 Rapptz, ThePhD and contributors
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "sol_test.hpp"
#include <catch2/catch_all.hpp>
#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <memory>
#include <set>
TEST_CASE("gc/destructors", "test if destructors are fired properly through gc of unbound usertypes") {
struct test;
static std::vector<test*> tests_destroyed;
struct test {
int v = 10;
~test() {
tests_destroyed.push_back(this);
}
};
test t;
test* pt = nullptr;
{
sol::state lua;
lua["t"] = test {};
pt = lua["t"];
}
REQUIRE(tests_destroyed.size() == 2);
REQUIRE(tests_destroyed.back() == pt);
{
sol::state lua;
lua["t"] = &t;
pt = lua["t"];
}
REQUIRE(tests_destroyed.size() == 2);
REQUIRE(&t == pt);
{
sol::state lua;
lua["t"] = std::ref(t);
pt = lua["t"];
}
REQUIRE(tests_destroyed.size() == 2);
REQUIRE(&t == pt);
{
sol::state lua;
lua["t"] = t;
pt = lua["t"];
}
REQUIRE(tests_destroyed.size() == 3);
REQUIRE(&t != pt);
REQUIRE(nullptr != pt);
}
TEST_CASE("gc/virtual destructors", "ensure types with virtual destroyions behave just fine") {
class B;
class A;
static std::vector<B*> bs;
static std::vector<A*> as;
class A {
public:
virtual ~A() {
as.push_back(this);
std::cout << "~A" << std::endl;
}
};
class B : public A {
public:
virtual ~B() {
bs.push_back(this);
std::cout << "~B" << std::endl;
}
};
{
sol::state lua;
lua.open_libraries(sol::lib::base);
lua.new_usertype<A>("A");
lua.new_usertype<B>("B", sol::base_classes, sol::bases<A>());
B b1;
lua["b1"] = b1; // breaks here
}
REQUIRE(as.size() == 2);
REQUIRE(bs.size() == 2);
}
TEST_CASE("gc/function argument storage", "ensure functions take references on their types, not ownership, when specified") {
class gc_entity;
static std::vector<gc_entity*> entities;
class gc_entity {
public:
~gc_entity() {
entities.push_back(this);
}
};
SECTION("plain") {
entities.clear();
sol::state lua;
lua.open_libraries();
sol::function f = lua.safe_script(R"(
return function(e)
end
)");
gc_entity* target = nullptr;
{
gc_entity e;
target = &e;
{
f(e);
lua.collect_garbage();
}
{
f(&e);
lua.collect_garbage();
}
{
f(std::ref(e));
lua.collect_garbage();
}
}
REQUIRE(entities.size() == 1);
REQUIRE(entities.back() == target);
}
SECTION("regular") {
entities.clear();
sol::state lua;
lua.open_libraries();
lua.new_usertype<gc_entity>("entity");
sol::function f = lua.safe_script(R"(
return function(e)
end
)");
gc_entity* target = nullptr;
{
gc_entity e;
target = &e;
{
f(e); // same with std::ref(e)!
lua.collect_garbage(); // destroys e for some reason
}
{
f(&e); // same with std::ref(e)!
lua.collect_garbage(); // destroys e for some reason
}
{
f(std::ref(e)); // same with std::ref(e)!
lua.collect_garbage(); // destroys e for some reason
}
}
REQUIRE(entities.size() == 1);
REQUIRE(entities.back() == target);
}
}
TEST_CASE("gc/function storage", "show that proper copies / destroyion happens for function storage (or not)") {
static int created = 0;
static int destroyed = 0;
static void* last_call = nullptr;
static void* static_call = reinterpret_cast<void*>(0x01);
typedef void (*fptr)();
struct x {
x() {
++created;
}
x(const x&) {
++created;
}
x(x&&) {
++created;
}
x& operator=(const x&) {
return *this;
}
x& operator=(x&&) {
return *this;
}
void func() {
last_call = static_cast<void*>(this);
};
~x() {
++destroyed;
}
};
struct y {
y() {
++created;
}
y(const x&) {
++created;
}
y(x&&) {
++created;
}
y& operator=(const x&) {
return *this;
}
y& operator=(x&&) {
return *this;
}
static void func() {
last_call = static_call;
};
void operator()() {
func();
}
operator fptr() {
return func;
}
~y() {
++destroyed;
}
};
// stateful functors/member functions should always copy unless specified
{
created = 0;
destroyed = 0;
last_call = nullptr;
{
sol::state lua;
x x1;
lua.set_function("x1copy", &x::func, x1);
auto result1 = lua.safe_script("x1copy()", sol::script_pass_on_error);
REQUIRE(result1.valid());
REQUIRE(created == 2);
REQUIRE(destroyed == 0);
REQUIRE_FALSE(last_call == &x1);
lua.set_function("x1ref", &x::func, std::ref(x1));
auto result2 = lua.safe_script("x1ref()", sol::script_pass_on_error);
REQUIRE(result2.valid());
REQUIRE(created == 2);
REQUIRE(destroyed == 0);
REQUIRE(last_call == &x1);
}
REQUIRE(created == 2);
REQUIRE(destroyed == 2);
}
// things convertible to a static function should _never_ be forced to make copies
// therefore, pass through untouched
{
created = 0;
destroyed = 0;
last_call = nullptr;
{
sol::state lua;
y y1;
lua.set_function("y1copy", y1);
auto result1 = lua.safe_script("y1copy()", sol::script_pass_on_error);
REQUIRE(result1.valid());
REQUIRE(created == 1);
REQUIRE(destroyed == 0);
REQUIRE(last_call == static_call);
last_call = nullptr;
lua.set_function("y1ref", std::ref(y1));
auto result2 = lua.safe_script("y1ref()", sol::script_pass_on_error);
REQUIRE(result2.valid());
REQUIRE(created == 1);
REQUIRE(destroyed == 0);
REQUIRE(last_call == static_call);
}
REQUIRE(created == 1);
REQUIRE(destroyed == 1);
}
}
TEST_CASE("gc/same type closures", "make sure destroyions are per-object, not per-type, by destroying one type multiple times") {
static std::set<void*> last_my_closures;
static bool checking_closures = false;
static bool check_failed = false;
struct my_closure {
int& n;
my_closure(int& n) : n(n) {
}
~my_closure() noexcept(false) {
if (!checking_closures)
return;
void* addr = static_cast<void*>(this);
auto f = last_my_closures.find(addr);
if (f != last_my_closures.cend()) {
check_failed = true;
}
last_my_closures.insert(f, addr);
}
int operator()() {
++n;
return n;
}
};
int n = 250;
my_closure a(n);
my_closure b(n);
{
sol::state lua;
lua.set_function("f", a);
lua.set_function("g", b);
checking_closures = true;
}
REQUIRE_FALSE(check_failed);
REQUIRE(last_my_closures.size() == 2);
}
TEST_CASE("gc/usertypes", "show that proper copies / destroyion happens for usertypes") {
static int created = 0;
static int destroyed = 0;
struct x {
x() {
++created;
}
x(const x&) {
++created;
}
x(x&&) {
++created;
}
x& operator=(const x&) {
return *this;
}
x& operator=(x&&) {
return *this;
}
~x() {
++destroyed;
}
};
SECTION("plain") {
created = 0;
destroyed = 0;
{
sol::state lua;
x x1;
x x2;
lua.set("x1copy", x1, "x2copy", x2, "x1ref", std::ref(x1));
x& x1copyref = lua["x1copy"];
x& x2copyref = lua["x2copy"];
x& x1ref = lua["x1ref"];
REQUIRE(created == 4);
REQUIRE(destroyed == 0);
REQUIRE(std::addressof(x1) == std::addressof(x1ref));
REQUIRE(std::addressof(x1copyref) != std::addressof(x1));
REQUIRE(std::addressof(x2copyref) != std::addressof(x2));
}
REQUIRE(created == 4);
REQUIRE(destroyed == 4);
}
SECTION("regular") {
created = 0;
destroyed = 0;
{
sol::state lua;
lua.new_usertype<x>("x");
x x1;
x x2;
lua.set("x1copy", x1, "x2copy", x2, "x1ref", std::ref(x1));
x& x1copyref = lua["x1copy"];
x& x2copyref = lua["x2copy"];
x& x1ref = lua["x1ref"];
REQUIRE(created == 4);
REQUIRE(destroyed == 0);
REQUIRE(std::addressof(x1) == std::addressof(x1ref));
REQUIRE(std::addressof(x1copyref) != std::addressof(x1));
REQUIRE(std::addressof(x2copyref) != std::addressof(x2));
}
REQUIRE(created == 4);
REQUIRE(destroyed == 4);
}
}
TEST_CASE("gc/double-deletion tests", "make sure usertypes are properly destroyed and don't double-delete memory or segfault") {
class crash_class {
public:
crash_class() {
}
~crash_class() {
a = 10;
}
private:
int a;
};
sol::state lua;
SECTION("regular") {
lua.new_usertype<crash_class>("CrashClass", sol::call_constructor, sol::constructors<sol::types<>>());
auto result1 = lua.safe_script(R"(
function testCrash()
local x = CrashClass()
end
)",
sol::script_pass_on_error);
REQUIRE(result1.valid());
for (int i = 0; i < 1000; ++i) {
lua["testCrash"]();
}
}
}
TEST_CASE("gc/shared_ptr regression", "metatables should not screw over unique usertype metatables") {
static int created = 0;
static int destroyed = 0;
struct test {
test() {
++created;
}
~test() {
++destroyed;
}
};
SECTION("regular") {
created = 0;
destroyed = 0;
{
std::list<std::shared_ptr<test>> tests;
sol::state lua;
lua.open_libraries();
lua.new_usertype<test>("test", "create", [&]() -> std::shared_ptr<test> {
tests.push_back(std::make_shared<test>());
return tests.back();
});
REQUIRE(created == 0);
REQUIRE(destroyed == 0);
auto result1 = lua.safe_script("x = test.create()", sol::script_pass_on_error);
REQUIRE(result1.valid());
REQUIRE(created == 1);
REQUIRE(destroyed == 0);
REQUIRE_FALSE(tests.empty());
std::shared_ptr<test>& x = lua["x"];
std::size_t xuse = static_cast<std::size_t>(x.use_count());
std::size_t tuse = static_cast<std::size_t>(tests.back().use_count());
REQUIRE(xuse == tuse);
}
REQUIRE(created == 1);
REQUIRE(destroyed == 1);
}
}
TEST_CASE("gc/double deleter guards", "usertype metatables internally must not rely on C++ state") {
SECTION("regular") {
struct c_a {
int xv;
};
struct c_b {
int yv;
};
auto routine = []() {
sol::state lua;
lua.new_usertype<c_a>("c_a", "x", &c_a::xv);
lua.new_usertype<c_b>("c_b", "y", &c_b::yv);
lua = sol::state();
lua.new_usertype<c_a>("c_a", "x", &c_a::xv);
lua.new_usertype<c_b>("c_b", "y", &c_b::yv);
lua = sol::state();
};
REQUIRE_NOTHROW(routine());
}
}
TEST_CASE("gc/alignment", "test that allocation is always on aligned boundaries, no matter the wrapper / type") {
struct test {
std::function<void()> callback = []() { std::cout << "Hello world!" << std::endl; };
void check_alignment() {
std::uintptr_t p = reinterpret_cast<std::uintptr_t>(this);
std::uintptr_t offset = p % std::alignment_of<test>::value;
REQUIRE(offset == 0);
}
};
sol::state lua;
lua.new_usertype<test>("test", "callback", &test::callback);
test obj {};
lua["obj"] = &obj;
INFO("obj");
{
auto r = lua.safe_script("obj.callback()", sol::script_pass_on_error);
REQUIRE(r.valid());
}
{
// Do not check for stack-created object
// test& lobj = lua["obj"];
// lobj.check_alignment();
}
lua["obj0"] = std::ref(obj);
INFO("obj0");
{
auto r = lua.safe_script("obj0.callback()", sol::script_pass_on_error);
REQUIRE(r.valid());
}
{
// Do not check for stack-created object
// test& lobj = lua["obj0"];
// lobj.check_alignment();
}
lua["obj1"] = obj;
INFO("obj1");
{
auto r = lua.safe_script("obj1.callback()", sol::script_pass_on_error);
REQUIRE(r.valid());
}
{
test& lobj = lua["obj1"];
lobj.check_alignment();
}
lua["obj2"] = test {};
INFO("obj2");
{
auto r = lua.safe_script("obj2.callback()", sol::script_pass_on_error);
REQUIRE(r.valid());
}
{
test& lobj = lua["obj2"];
lobj.check_alignment();
}
lua["obj3"] = std::make_unique<test>();
INFO("obj3");
{
auto r = lua.safe_script("obj3.callback()", sol::script_pass_on_error);
REQUIRE(r.valid());
}
{
test& lobj = lua["obj3"];
lobj.check_alignment();
}
lua["obj4"] = std::make_shared<test>();
INFO("obj4");
{
auto r = lua.safe_script("obj4.callback()", sol::script_pass_on_error);
REQUIRE(r.valid());
}
{
test& lobj = lua["obj4"];
lobj.check_alignment();
}
}
TEST_CASE("gc/multi-argument destructors", "make sure transparent arguments come along for the ride") {
static int transparent_foos_destroyed = 0;
struct transparent_foo {
~transparent_foo() {
++transparent_foos_destroyed;
}
};
lua_State* lua_state = nullptr;
lua_State* call_state = nullptr;
auto call_des = [&call_state](transparent_foo* f, sol::this_state s) {
call_state = s;
return f->~transparent_foo();
};
{
sol::state lua;
lua_state = lua;
lua.new_usertype<transparent_foo>("foo", sol::meta_function::garbage_collect, sol::destructor(std::move(call_des)));
lua.script("foo.new()");
}
REQUIRE(transparent_foos_destroyed == 1);
REQUIRE(call_state == lua_state);
}