Advertisement

AS deletes the object pointed to by handle

Started by November 19, 2010 01:02 PM
4 comments, last by Nightition 14 years ago
Hi!

I have a problem where if I store a handle to an object in a script class object, it gets deleted when it shouldn't.

So, the script looks like this:

class MainClass{  CEntity@ entity;    MainClass() {};  void Init(CEntity& ent)  {    @entity = ent;  }}


CEntity comes from C++, and is declared with behaviors of ADDREF and RELEASE, but no FACTORY or assignment operator.

It also has a defined implicit cast to a CBaseEntity, but CBaseEntity doesn't have any behaviors defined except ADDREF and RELEASE.

What I do is I create an object of type MainClass, and then store it inside of my CEntity (actually it's wrapped in another class, but in a pretty basic way). When I receive the object from the factory, I call AddRef on it.
Then I call its init method, sending 'this' as the parameter. The script works fine - any code I put in it that manipulates the entity works.
However, when deleting the entity, I call Release() on the script object, and then somehow it calls the destructor on my CEntity, so that destructor gets called twice.

It may sound like a wrong reference count, but that's not it. First of all, if I put a breakpoint in the destructor, the reference count of that object (CEntity) is 2 at that point. Second, there is a special flag in that object which prevents it from getting deleted even if its reference count reaches zero.

Basically, we didn't want to change our code to use reference counting, because that would be way too much work. So I've come up with a little trick: there is a bool flag in each of those reference counted objects, telling me whether it was created in the native code or in the script engine. If it was created in the native code, its "Release" method will simply not delete it, and the native code will be held responsible for deleting it eventually.

My stack trace at the point of the second call of the destructor contains this:

MapViewer.exe!CEntity::`scalar deleting destructor'()  + 0x2e bytes	C++...Core.dll!asCScriptEngine::CallObjectMethod()  + 0x15 bytes	C++Core.dll!asCScriptObject::~asCScriptObject()  + 0xae bytes	C++Core.dll!asCScriptObject::`vector deleting destructor'()  + 0x8 bytes	C++Core.dll!asCScriptObject::Release()  + 0x33 bytes	C++Core.dll!asCGarbageCollector::DestroyGarbage()  + 0xa5 bytes	C++Core.dll!asCGarbageCollector::GarbageCollect()  + 0x6c bytes	C++Core.dll!asCScriptEngine::GarbageCollect()  + 0xf bytes	C++


The garbage collector is called by me because I wanted it to immediately delete the just released script object because the Entity is about to become invalid and it holds a reference to it.

Am I doing something obviously wrong?

I don't think I understand how those object handles actually work, when things get copied, etc.

For reference, I've also tried:

in the Init method:

@entity = @ent;
entity = ent;
entity = @ent;

I've also tried Init(CEntity@ ent), with all of the above assignment combinations too. Some don't compile, and those that do result in the same crash.

Any ideas?


Sorry for the long post :(

The @entity = ent; statement will perform a handle assignment and call the ADDREF behaviour on the CEntity. It is implicitly converted to @entity = @ent; by the compiler, so that is identical.

entity = ent will perform a value assigment, i.e. call the opAssign method if available or give a compiler error if not available.

entity = @ent would attempt to perform a value assignment with a handle, which you probably don't have a matching opAssign method for.



When the MainClass is destroyed by the garbage collector, it will call the RELEASE behaviour on CEntity. AngelScript wouldn't never call the destructor of CEntity directly.



How are you calling the MainClass' Init method?

How did you register the CEntity?

What are the functions on the callstack between CEntity::~CEntity and asCScriptEngine::CallObjectMethod? At least the CEntity::Release should be there.

How are the CEntity::Release and CEntity::~CEntity implemented?

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
Quote:
The @entity = ent; statement will perform a handle assignment and call the ADDREF behaviour on the CEntity. It is implicitly converted to @entity = @ent; by the compiler, so that is identical.

entity = ent will perform a value assigment, i.e. call the opAssign method if available or give a compiler error if not available.

entity = @ent would attempt to perform a value assignment with a handle, which you probably don't have a matching opAssign method for.


Thanks for the clarifications, it makes more sense now. One other question - regarding references - the documentation says AS will make a copy of the object if it can't guarantee the validity of the reference. What exactly does that mean? And how is it going to behave with a type which doesn't have an assignment operator registered?

Quote:
How are you calling the MainClass' Init method?



	Void CallInit(asIScriptContext *ctx, CEntity *entity)	{		asIObjectType *type = pScriptObject->GetObjectType();		int funcId = type->GetMethodIdByDecl("void Init(CEntity@)");		if (funcId >= 0)		{			ctx->Prepare(funcId);			ctx->SetObject(pScriptObject);		    entity->AddRef();			ctx->SetArgObject(0, (void*)str);			ctx->Execute();		}	}


Quote:
How did you register the CEntity?


It's asOBJ_REF type, has ADDREF and RELEASE behaviors. There's also an implicit cast defined to CBaseEntity, but that one just exposes some methods (I'm using templated functions to register base type, like in the docs).


Quote:
What are the functions on the callstack between CEntity::~CEntity and asCScriptEngine::CallObjectMethod? At least the CEntity::Release should be there.


That's the strangest part - I don't see the release method in there. There is a part of the stack trace there which I've left out, but that's in a separate DLL I don't have the source to. However, the release method doesn't get called - I know because I don't reach it by putting a breakpoint in there, and there's a log statement in that method too, which I don't see in the output.

Quote:
How are the CEntity::Release and CEntity::~CEntity implemented?


The release method works like this:
	virtual Void Release() const	{		Log->Write(Log_Error, "%p Releasing %d", this, refCount);		if ((--refCount <= 0) && !engineOwned)		{			delete this;		}	}


However, 'engineOwned' is _always_ true in the case of CEntity, because they are only created from the native code.

~CEntity looks like this:

	Log->Write(Log_Error, "Deleting entity: %d", (int)(this));	DeInit();


While DeInit looks like this:
...	Log->Write(Log_Error, "Deinitializing entity: %d", (int)(this));	if (pScriptObject)	{		Del(pScriptObject);		pScriptObject = NULL;	}...


In the log, I get two "Deinitializing..." messages, and one "Deleting...". Considering the call stack, it looks like it calls the destructor for some reason when it wants to call "Release" (I'm assuming this CallObjectMethod here is trying to call Release). Maybe some kind of heap corruption... Is it possible something gets messed up because of multiple inheritance? So that the Release method and the destructor get mixed up in some way? Because although CEntity only inherits CBaseEntity, that one or the one above inherits multiple classes.

In any case, thanks for the help, and thanks for Angelscript!

Ivan
Quote: Original post by Nightition
Thanks for the clarifications, it makes more sense now. One other question - regarding references - the documentation says AS will make a copy of the object if it can't guarantee the validity of the reference. What exactly does that mean? And how is it going to behave with a type which doesn't have an assignment operator registered?


The copy will only be made for in references. inout references always point to the real object. The copy is done if the object is not a local variable, for example if it is an element of an array that might get resized during the call thus invalidating the reference.

The compiler would give an error if it sees it is necessary to make a copy but it can't.



Quote:
*** Source Snippet Removed ***


The entity->AddRef() is not necessary and the SetArgObject() already does that for you. This probably explains why you see a refcount = 2 in the destructor.

Where did the str come from?


Quote:
That's the strangest part - I don't see the release method in there. There is a part of the stack trace there which I've left out, but that's in a separate DLL I don't have the source to. However, the release method doesn't get called - I know because I don't reach it by putting a breakpoint in there, and there's a log statement in that method too, which I don't see in the output.

...

In the log, I get two "Deinitializing..." messages, and one "Deleting...". Considering the call stack, it looks like it calls the destructor for some reason when it wants to call "Release" (I'm assuming this CallObjectMethod here is trying to call Release). Maybe some kind of heap corruption... Is it possible something gets messed up because of multiple inheritance? So that the Release method and the destructor get mixed up in some way? Because although CEntity only inherits CBaseEntity, that one or the one above inherits multiple classes.



There is definitely something strange going on. CallObjectMethod should call Release directly, which as you say shouldn't delete the object.

I guess you have some kind stack corruption, possibly the function pointer for the Release behaviour is incorrect. Or, if the Release behaviour is a virtual method, and you have an incorrect pointer cast somewhere AngelScript may call the wrong method as the virtual function table in the C++ object would be wrong.

That you get two "Deinitializing" and only one "Deleting" also indicates a stack problem. Only the destructor should be calling the DeInit function, right?

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

It's very strange indeed.
Just moving the reference counting methods down the hierarchy (just one class, from CScriptedObject to CCoreClass, which inherits only CScriptedObject), made the crash disappear. Even more strange, while debugging a valid object would suddenly get deleted over a single step (and we're not doing any multithreading with this code).
Making the ref counting methods non-virtual kind of relocated the crash to another place, etc.

One thing that's the current suspect is the "VCF", or Visual Component Framework, which is an open-source project and which contains a lot of "magic" in its code (such as an RTTI implementation we use and also its own reference counting), so that's where we're investigating now.

In any case, thanks a lot for the help; at least now I understand better what I'm doing with AS, and once we fix the bug I'll make sure to share here if it turns out it may be of value to future AS users.

Ivan

P.S. That 'str' was just a leftover from my experimenting with a different type of object (which doesn't crash the system), it should be 'entity' instead.

EDIT:

Quote:
There is definitely something strange going on. CallObjectMethod should call Release directly, which as you say shouldn't delete the object.


Yes, but we don't see that in the stack trace. I'm pretty sure the method addresses get mixed up somehow. I've removed the "delete" from out Release, but it didn't change anything.

Quote:
I guess you have some kind stack corruption, possibly the function pointer for the Release behaviour is incorrect. Or, if the Release behaviour is a virtual method, and you have an incorrect pointer cast somewhere AngelScript may call the wrong method as the virtual function table in the C++ object would be wrong.


Could be, although I haven't been able to find any of these problems.

Quote:
That you get two "Deinitializing" and only one "Deleting" also indicates a stack problem. Only the destructor should be calling the DeInit function, right?


Not really. EntityManager may reuse an entity, by calling DeInit (and then Init again at some point maybe), and DeInit also gets called from the destructor. This crash happens at the game shutdown time, so EntityManager does the DeInit, but from that DeInit (when deleting our script object) somehow delete gets called and then DeInit is called again from the destructor.
As you had guessed, the problem was a wrong pointer. I'm still posting it here simply because I see this error very often, especially in older code.

In one place, a C-style cast was used when downcasting from VCF::Object to CEntity, which messed up method addresses and caused AngelScript to call the wrong method.
VCF's RTTI implementation returns a VCF::Object, and we did something like:

CEntity *entity = (CEntity*)VCF::CreateObjectFromName("CCameraEntity");

Replacing that with C++ dynamic_cast solved the problem.

Again, thanks for the help and for AngelScript! :)

Ivan

This topic is closed to new replies.

Advertisement