Advertisement

Checking reference count of script objects

Started by September 10, 2018 08:52 AM
8 comments, last by WitchLord 6 years, 2 months ago

Hello,

I was convinced this will be as easy as calling some method of asIScriptObject but it turns out that there is no method to retrieve current refCount of a given object? I mean the ones created from within scripts, not C++ side ones which can register AddRef/Release behaviours and store this somewhere in accessible place. Wouldn't it be useful to be able to dump refs for existing objects for debugging purposes, or checking whether there is just one owner and we could release the resource (some more advanced ownership schemes may be needed even with shared ownership by references in script, and some things like removing object from world may only happen when the world is the last remaining holder of the reference and so on). 

Any particular reason why this is not provided by script object interface? I noticed that there is such method inside the class hierarchy there, but it's just not exposed by asIScriptObject...

Thanks!


Where are we and when are we and who are we?
How many people in how many places at how many times?

If you really need to know the current refCount of script objects you can do so by calling AddRef and then Release. The returned value from Release is the current refCount.

I can see the usefulness of this for debugging purposes, but not really for anything else. Having your code continously poll the current refCount of certain objects in order to take a decision doesn't sound very efficient. Are you this is really what you want?

I would recommend not to use any game logic that relies on the refCount of any objects, as objects may live longer than intuitively expected due to circular references that may take several iterations to be identified and destroyed by the garbage collector. And under no circumstances should you be calling Release on an object that you don't own as it would very likely lead to crashing your application when the actual owner will attempt to access the object that was destroyed prematurely by your call to Release.

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

I'm generally struggling with ownership scheme for my game, and was hoping that refCount check would be a way to seal my idea finally. The main problem I have is removing object from game world which should happen when the object is "destroyed". Let me describe what I have now, maybe someone can suggest some idea as I'm totally lost between several approaches, none of whose seems to work 100%.

1. I use ProxyObject on C++ side which has some functionality and is used by script object (it's created together with script object and kept alive as long as script object exists). So this is very similar to what you have in Game example, but I only allow creation of objects through special factory function defined on C++ side, so you can't do


Object @newobject = Object() 

inside script as the ctor is private and you can only construct new object by calling


createObject('/some/file.as')

 inside scripts.

2. So, inside scripts I use Object base class, it's also inherited by more specialized derived classes like Item, Monster etc. (very simplified, it also makes heavy use of mixins). On C++ side, ProxyObject is inserted into a list of "world objects" and is iterated over, and provides a bridge between C++ and scripting. It's for example used to call methods like tick() on script objects and so on. 

3. It's important to note that I keep one reference on C++ side to such script objects, so they stay alive when nothing else refers to them. This is kind of main ownership when object is put into the world (the World references it). Other situations is when the object is contained inside other object - that object then holds the reference to it's children. 

3. Various objects will have to refer to other objects somehow and this is where the idea starts to have some weak spots. If I use references to script objects, I'm risking that I won't be able to really destroy the object because if any other object keeps reference to another object, it will keep it alive indefinitely (as long as it's not letting it go). Imagine situation where I want to keep reference to whom attacked me last so I have member Object @lastAttackedBy - once I store it, it adds reference to that object and if we do a coding mistake and keep it while the referred object is destroyed (so World releases it's reference and assumes that the object should now go away) we have problem - as long as that lastAttackedBy is not cleared we have object hanging in limbo, as it should not exist but it does and it can be called on, which may lead to weird situations (object is already removed from the world).

4. So above example shows that references cause a lot of headache when it comes to finalizing life of an object. Not sure how to deal with this. Weakrefs may help a bit, but nothing stops anyone from holding real reference and this won't be easily caught (here comes my question about verying refcount and checking if destroyed objects are not held alive against the logic)

I know this is probably some common problem, I searched around and haven't found any solution that would apply here. Some people use handles (as in, just some int id for the object) and re-request them each time they're needed, which does not keep reference and allows us to get null if the object is already gone. I'm considering this but it adds some overhead in code when we need to always fetch the object before we can use it. But this may be simpler to manage than the reference hell :(

Any ideas welcome! How do others manage game object lifetime - and I'm talking about more complex situation where objects come and go during the game and need to be taken care of properly to not cause weird behaviour. 

 

 

 

 


Where are we and when are we and who are we?
How many people in how many places at how many times?

What I do in my prototype game engine is that I have a "proxy" object (I call it GameObjectLink) that gives the link from scripts to the true C++ "game object". No script holds any reference directly to the C++ "game object", so malformed scripts cannot keep the C++ "game object" alive.

The C++ game object keeps a reference to the proxy object and to the script controller object. As long as the C++ game object is alive there is a cyclic reference "script controller" -> "proxy" <-> "game object" -> "script controller". Other references to the "proxy" can also exists (e.g. when holding a reference to @lastAttackedBy), but no other references to the "script controller". 

When the game needs to remove a "game object" (destroy it), the link between the "proxy" and the "game object" and the link to the "script controller" is released. After this the "game object" is guaranteed to have no other references to it and can be safely destroyed. The "proxy" can still have references to it and won't necessarily be destroyed immediately, but it is now only an empty shell, and won't cause any harm. The "script controller" itself will be destroyed since on the "game object" held references to it. The only exception is if some malformed script is keeping a link to the "script controller" somewhere, but since the "script controller" can no longer affect the "game object" there is no harm in that, and the garbage collector will eventually free the lingering "script controller".

The "proxy" itself has no real logic in it. So while there is a little run-time overhead in this solution, the gain in simplicity of the memory mangement greatly outweighs that overhead.

 

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

Thanks, I'm now exploring your engine - this seems like it could work - I'd still have a way to keep script object alive due to code error but it won't have much effect on anything in game world since all the real representation on C++ side will be gone. Thanks a lot!


Where are we and when are we and who are we?
How many people in how many places at how many times?

Okay I think I see what you did there - makes a lot of sense, even at the cost of having two objects on C++ side and some "boilerplate" code to stitch real object with the link. But I think it will work nicely with my component system too, because in my counterpart of your GameObjectLink I can define another type of call which will

  • check if obj is not null ➡️raise exception "Object destroyed" if it's null
  • check if obj has component N ➡️raise Exception "Object has no component N" if it doesn't
  • call function on a given component (for example, Transform->getPosition())

What you think about the idea of raising exceptions in these cases rather than returning default (empty) values from funcs or doing nothing? I think it makes it more obvious that there is some dangling reference to the object that's trying to do something with it, and it should be handled in the code, either by holding weakref or by checking the object for validity, like:


if (obj.isValid())
{
    pos = obj.getPosition();
    do_something(pos);
}

if we have some chance that the object got destroyed while we kept reference to it?

I think I could probably get away with no GameObjectLink and just setting null to the C++ proxy reference inside the script when it gets destroyed - this would cause null pointer access on script side, so will be rather safe and explicit (goes to logs). But I like the separation of actual GameObject and the proxy object, so I might extend my implementation to follow the same approach, thanks for inspiration!

Additional thing that I plan to have to somehow improve programmer life is to hide the "entity" or proxy object from user on script side. Right now in your implementation you always call C++ funcs on entity like entity.SetTimer() etc. which is okay but it could be wrapped by the object itself. I have it right now and it hides the C++ object completely like this:


shared abstract class Object: IScriptObject
{
    private Object()
    {
       @obj = getProxyObject();   // a trick to not pass object in ctor but set it within C++ when creating
                                  // script object, allows to not require to remember about this additional
                                  // thing to be passed in ctor
    }

    void destroy()
    {
       obj.destroy()
    }
  
    string getId()
    {
       return obj.getId();
    }

    private ProxyObject@ obj;
}

So I kind of wrap calls to entity/obj in abstract class, meaning the link is hidden from any script user - it's abstract class with private member. Then I can do this:


class Zombie: Object
{
    void tick()
    {
       if (dead)
       {
          destroy();
       }
    }
}

No need to remember about entity indirection, you can enrich the script API by additional checks, make some of those things properties etc. plus no need for specifying any ctors & IScriptObject inheritance for C++ sake. I hope this idea is useful to anyone. It's additional "boilerplate" to write but end result is clean and hides a lot of unnecessary code from end user.

 

Thanks for the idea with GameObjectLink! :)


Where are we and when are we and who are we?
How many people in how many places at how many times?
Advertisement

Raising exceptions or returning default values is really a matter of taste. I try to use exceptions only when it truly is an exception to normal behaviour in which case the exception really should abort the execution, but others like to use exception even for most trivial errors, in which case I think all the catch logic tend to pollute the code.

With the latest WIP version I've added support for try/catch statements as well if you prefer to allow that. Though you can also raise a non-catchable exception if you don't want the scripts to be able to catch them.

Your abstract class Object is similar to the design written for 'Inheriting from application registered class'. :)

 

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

To be honest I use exception there only for two reasons: 1) because it's caught by my logger that spits something like this:


 Exception: Accessing destroyed object!
 File: dsrc/std/player.as
 Function: string Object::getId()
 Line: 35

so it's easy to find where the problem occurs. I don't think it should really be handled by the code in this case, it's more of a runtime indication that some code holds references too long and should convert to either weakref or fix the problem in some other way. Happy to see try/catch implemented though! It may not have to be handled but in case someone wants - there is a possibility now.

2) to break the execution of the invalid state object - which I consider one that references destroyed object and tries to act on it. This also means I don't have to return some default data just to satisfy return type, which allows callee to act on this value while it really should not proceed at all, as the object is gone.

So, to sum up, I rather consider this kind of behaviour to be debug-only and expected to be fixed when encountered in logs rather than handled by the code, if that makes sense :)

Regarding inheriting from registered class, yes, I based my idea on this but extended it a bit (added interface to query from C++ & changed initialization of proxy object to not use ctor directly but allow "injecting" that value thru function call in ctor - something we discussed some time ago, as my proxy object is constructed differently and I needed to pass existing one rather than create new. Now when I'm going to implement GameObjectLink I think I will be able to revert to creating proxy directly in abstract GO ctor like in your example.


Where are we and when are we and who are we?
How many people in how many places at how many times?

Yes, your usage of script exceptions is also how I would use it, that is to identify cases of badly performing scripts, log the case and abort the script in a graceful way.

 

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

This topic is closed to new replies.

Advertisement