Advertisement

Memory leak

Started by August 09, 2007 02:25 PM
4 comments, last by mostly_human 17 years, 3 months ago
I think I found a memory leak when script objects get deleted. If there is a c++ class Test and an angel script class Foo which contains a reference to Test then when Foo goes out of scope Test is not released. main.cpp:

#include <iostream>
#include "scriptstring.h"
#include "Test.h"
#include <assert.h>

void MessageCallback(const asSMessageInfo* msg, void* param);

int main()
{
    asIScriptEngine* eng = asCreateScriptEngine( ANGELSCRIPT_VERSION);
    int r;
    r = eng->SetMessageCallback( asFUNCTION( MessageCallback), NULL, asCALL_CDECL); assert( r >= 0);
    Test::Register( eng);

    std::string script;
    //Foo goes out of scope m_Test needs to be released
    script += "class Foo\n";
    script += "{\n";
    script += "    Test@ m_Test;\n";
    script += "};\n";
    script += "void main()\n";
    script += "{\n";
    script += "    Foo foo;\n";
    script += "    Test t;\n";
    script += "    @foo.m_Test = @t;\n";
    script += "}\n";

    r = eng->AddScriptSection( NULL, NULL, script.c_str(), script.size()); assert( r >= 0);
    r = eng->Build( 0); assert( r >= 0);

    asIScriptContext* context = eng->CreateContext();

    //this always outputs 0
    std::cout << "Num before: " << Test::NumLeft() << "\n";

    r = eng->ExecuteString( "", "main()", &context); assert( r >= 0);

    //this should output 0 but outputs 1
    std::cout << "Num after: " << Test::NumLeft() << "\n";

    context->Release();
    eng->Release();
    return 0;
}

void MessageCallback( const asSMessageInfo* msg, void* param)
{
	const char *type = "ERR ";
	if( msg->type == asMSGTYPE_WARNING )
	{
		type = "WARN";
	}
	else if( msg->type == asMSGTYPE_INFORMATION )
	{
		type = "INFO";
	}

	printf("%s (%d, %d) : %s : %s\n", msg->section, msg->row, msg->col, type, msg->message);
}

Test.h:

#include "angelscript.h"
#include <vector>

/**
* Simple script class.
* Provides basic reference counting.
*/
class Test
{
public:
    Test();
    Test( const Test& test);
    virtual ~Test();

    /**
    * Increments the reference count.
    */
    void add();

    /**
    * Deincrements the reference count.
    * When the reference count reaches 0 this is no longer valid.
    */
    void release();

    /**
    * Calls the destructor.
    */
    virtual void destroy();

    /**
    * Check how many Test objects are in existance.
    *
    * @return Number of Test objects.
    */
    static size_t NumLeft();

    /**
    * Register to angelscript.
    *
    * @param eng Engine to register to.
    */
    static void Register( asIScriptEngine* eng);
private:
    /**
    * Holds all existing instances of Test.
    * Used to make sure there are no memmory leaks.
    */
    static std::vector<Test*> s_Test;
    unsigned int m_RefCount;    /**< Reference counter. */
};

Test.cpp:

#include "Test.h"
#include <algorithm>
#include <iostream>
#include <assert.h>

std::vector<Test*> Test::s_Test;

Test::Test()
{
    std::cout << "new\n";
    m_RefCount = 1;
    s_Test.push_back( this);
}

Test::Test( const Test& test)
{
    std::cout << "new2\n";
    m_RefCount = 1;
    s_Test.push_back( this);
}

Test::~Test()
{
    std::vector<Test*>::iterator iter;
    iter = std::find( s_Test.begin(), s_Test.end(), this);
    if( iter == s_Test.end())
    {
        std::cout << "Deleted twice\n";
        std::exit( 1);
    } else {
        s_Test.erase( iter);
    }
}

void Test::add()
{
    std::cout << "add\n";
    m_RefCount++;
}

void Test::release()
{
    std::cout << "release\n";
    assert( m_RefCount > 0);
    m_RefCount--;
    if( m_RefCount <= 0)
    {
        delete this;
    }
}

void Test::destroy()
{ this->~Test(); }

size_t Test::NumLeft()
{
    return s_Test.size();
}

void Construct( Test* obj)
{ new (obj)Test(); }

Test& Copy( Test& obj, const Test& other)
{
    return obj = other;
}

void Test::Register( asIScriptEngine* eng)
{
    int r;
    r = eng->RegisterObjectType( "Test", sizeof( Test), asOBJ_CLASS_CDA); assert( r >= 0);
    r = eng->RegisterObjectBehaviour( "Test", asBEHAVE_CONSTRUCT, "void f()", asFUNCTION( Construct), asCALL_CDECL_OBJFIRST); assert( r >= 0);
    r = eng->RegisterObjectBehaviour( "Test", asBEHAVE_DESTRUCT, "void f()", asMETHOD( Test, destroy), asCALL_THISCALL); assert( r >= 0);
    r = eng->RegisterObjectBehaviour( "Test", asBEHAVE_ASSIGNMENT, "Test& f(const Test&in)", asFUNCTION( Copy), asCALL_CDECL_OBJFIRST); assert( r >= 0);
    r = eng->RegisterObjectBehaviour( "Test", asBEHAVE_ADDREF, "void f()", asMETHOD( Test, add), asCALL_THISCALL); assert( r >= 0);
    r = eng->RegisterObjectBehaviour( "Test", asBEHAVE_RELEASE, "void f()", asMETHOD( Test, release), asCALL_THISCALL); assert( r >= 0);
}

I'll look into this. Thanks for the sample code.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Advertisement
Can you share the output from your test? (I don't want to compile it myself, and I'm probably wrong anyway)
My output (windows gcc):
Num before: 0
new
add
add
release
release
Num after: 1

My guess is that when a script object goes out of scope the references are not set to null. If "@foo.m_Test = null;\n" is added to the end of main then the new output is:
Num before: 0
new
add
add
release
release
release
Num after: 0
Turns out it's not a memory leak after all. The script class just isn't freed when you thought it would be freed.

Because this script class has an object handle as a member, it has been marked as an object that could potentially form circular references. Circular references cannot be freed just by reference counting, so the AngelScript garbage collection holds a reference to the object as well, so that it can check for circular references when invoked.

Since you're not explicitly invoking the garbage collector in your sample the script class is only released as you release the engine, which is also when the application registered object is freed.

You'll see that the object is correctly released if you add a call to eng->GarbageCollect(true); just before you check how many Test objects are alive. (true means that the garbage collector will do a full cycle, instead of just a small step.)

I'd also like to make a couple of comments on your test code:

1. You don't have to register the destructor behaviour for this class, since it will never be called by AngelScript, because the release behaviour is used instead.

2. Your assignment behaviour isn't implemented correctly for the Test class. You're doing a bitwise copy of the object, which would also overwrite the reference counter, which can lead to memory leaks or premature deallocations of the class.

Regards,
Andreas

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Thank you very much for you help. Sorry, I should have read the documentation again before posting. I'll go fix that bug in my test class before I start seeing more bugs that are not there [smile].

This topic is closed to new replies.

Advertisement