CSpaceObjectCreation.cpp

// CSpaceObjectCreation.cpp
//
// Authors: David Barrett-Lennard, Jesse Pepper
// (C)opyright Cedanet Pty Ltd 2007

@import "Ceda/cxObject/Object.h"
@import "Ceda/cxObject/IObjectVisitor.h"
@import "Ceda/cxObject/WCSpace.h"
@import "Ceda/cxObject/gcroot.h"
@import "ExampleUtils.h"
#include "Ceda/cxUtils/HPTime.h"
#include <assert.h>
#include <memory>

///////////////////////////////////////////////////////////////////////////////////////////////////
// CSpaceObjectCreation1
/*
Object creation
---------------

This example concerns the creation of objects within a CSpace.  In order for an object to be
registered with the CSpace (and therefore take part in garbage collection) it must be heap 
allocated, typically using the $new syntax.

There are two requirement to using $new

    1.  The calling thread must be affiliated with the appropriate CSpace
    2.  A shared read or exclusive lock has been gained on the CSpace.

When a class implements IObject it's not mandatory for it to be registered with a CSpace 
(nor even to be heap allocated).  It is permissible for it to be a member of another class, 
a global variable, a frame variable etc.
*/
namespace CSpaceObjectCreation1
{
	$interface+ Ix : ceda::IObject
	{
	    void Foo();
	};

	$struct+ X isa Ix
	{
	    void Foo() { Tracer() << "Foo\n"; }
	};
	
	$struct+ Y isa ceda::IObject
	{
	    $X x;
	    $ceda::int32 i;
	};

	void Run()
	{
		ceda::TraceGroup g("CSpaceObjectCreation1");
		
        ceda::CSpaceCreator cspace;
        ceda::CSpaceLock lock(ceda::ECSpaceLockMode::Exclusive);

        // Creation of an X that is registered with the thread's affiliated CSpace.
        {
            // Approach 1
            ceda::ptr<Ix> p1 = ceda::qicast<Ix>( ceda::TryFindReflectedClass("CSpaceObjectCreation1::X")->createFn());
            ceda::RegisterGcObject(p1);
            p1->Foo();

            // Approach 2
            ceda::ptr<Ix> p2 = ceda::qicast<Ix>( ceda::GetReflectedClass<X>().createFn());
            ceda::RegisterGcObject(p2);
            p2->Foo();

            // Approach 3
            ceda::ptr<Ix> p3 = ceda::qicast<Ix>(ceda::CreateInstance<X>());
            p3->Foo();

            // Approach 4
            ceda::ptr<Ix> p4 = $new X;
            p4->Foo();
        }
        
        // Creation of an X that is not registered with the thread's affiliated CSpace.
        {
            // Declaration of an X as a variable on the frame doesn't register it with the CSpace
            {
                X x;
            }

            // Newing an X directly from the heap doesn't register it with the CSpace
            X* px = new X;
            delete px;

            // An X can appear as a member of another class Y.  When a Y is created and registered with
            // the CSpace the embedded X member is not treated as a separate object by the CSpace.
            {
                ceda::ptr<ceda::IObject> y = ceda::CreateInstance<Y>();
            }
        }
	}
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// CSpaceObjectCreationWithSharedReadLock
namespace CSpaceObjectCreationWithSharedReadLock
{
    $interface+ Ix : ceda::IObject
    {
        void Foo();
    };

    $struct X isa Ix
    {
        void Foo() { Tracer() << "Foo\n"; }
    };
	
    void Run()
    {
	    ceda::TraceGroup g("CSpaceObjectCreationWithSharedReadLock");
		
        ceda::CSpaceCreator cspace;
        ceda::CSpaceLock lock(ceda::ECSpaceLockMode::SharedRead);

        // Objects can be registered with only a shared read lock on the CSpace
        // This is important for objects that represent cached values.
        ceda::ptr<Ix> p = $new X;
        p->Foo();
    }
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// gcrootExample
/*
It is commonly the case that pointers declared on the frame need to be visited in order to 
pin objects for the life of the declaration.  A gcroot can be used for this purpose.
*/
namespace gcrootExample
{
    $struct X isa ceda::IObject
    {
        X() { Tracer() << "X()\n"; x = 1; }
        ~X() { Tracer() << "~X()\n"; x = 0; }
        
        void foo() { Tracer() << "Foo\n"; cxAssert(x == 1); }
        int x;
    };
	
    void Run()
    {
  	    ceda::TraceGroup g("gcrootExample");
        ceda::CSpaceCreator cspace(20);     // GC every 20 msec
        ceda::gcroot<X*> p;
        {
            ceda::CSpaceLock lock;        // i.e. exclusive lock
            p.reset($new X);
        }
        Sleep(100);    // Wait long enough for a GC
        p->foo();
    }
}

/*
Some comments about the philosphy of ceda locking
-------------------------------------------------

In ceda1 locks were typically done by the framework, such as in a windows procedure.  
The idea was that the programmer could mostly assume that a lock was normally already 
in place and therefore often ignores the need to define transaction boundaries.

By contrast in ceda2, it is now assumed that the programmer must carefully define 
exactly where transactions (or locks) are declared, and they should preferably be as 
short-lived as possible and relate to a very specific, well defined and well 
understood action.  This action should be atomic, meaning that it doesn't make sense 
to split it into smaller pieces.  For example, rendering a frame using OpenGL needs 
a consistent snapshot of the models, and in that sense the transaction cannot be 
split into smaller pieces.  By the same token, during the scope of the lock, the 
only concern should be drawing the scene.  That means for example, don't try to start 
or stop any threads!

That is why the dead-lock problem is regarded as a symptom of poor design (more 
specifically, not paying attention to what units of work should be done within a 
transaction).

For example, if you write a function to import an image into a folder don't assume there 
is already a lock, and do as much of the work as possible without a lock.  Only open 
a transaction at the end in order to insert the new object into the tree.   This way 
of thinking has nothing to do with whether that function is called by a worker thread 
or the main GUI thread!

The following example shows how a thread can have its own CSpace stack in order to 
facilitate this pattern.
*/
namespace gcrootExample2
{
    $struct Folder isa ceda::IObject
    {
        void VisitObjects(ceda::IObjectVisitor& v) const
        {
            Tracer() << "Visiting " << m_children << '\n';
            v << m_children;
        }
        void Add(ceda::ptr<ceda::IObject> p) 
        {
            cxAssert(GetAssociatedCSpace(p) == GetAssociatedCSpace(this));
            
            m_children.push_back(p);
        }
        ceda::xvector<ceda::ptr<ceda::IObject> > m_children;
    };
    
    $struct Image isa ceda::IObject
    {
        // Called without a lock
        void LoadFromDisk(ceda::ConstStringZ path) 
        {
            Sleep(100);
        }
    };
	
    // Must be called without a lock
    void ImportImage(Folder* f, ceda::ConstStringZ path)
    {
        ceda::gcroot<Image*> i;

        // Atomic unit of work:  Allocate an object and protect it from GC
        {
            ceda::CSpaceLock lock;
            i.reset($new Image);
        }
        
        // Called without a lock!
        i->LoadFromDisk(path);

        // Atomic unit of work:  Generate operation to insert object 
        // into the containing folder
        {
            ceda::CSpaceLock lock;
            f->Add(i.get());
        }
    }

    // Note that the above example suggests that $new is a poor building block.
    void ImportImage2(Folder* f, ceda::ConstStringZ path)
    {
        std::unique_ptr<Image> i(new Image);
        i->LoadFromDisk(path);
        {
            ceda::CSpaceLock lock;
            
            // Transfer ownership from the unique_ptr to the CSpace
            Image* p = i.release();
            RegisterGcObject(p);   // $new normally calls this
            
            f->Add(p);
        }
    }
    
    /*
    It is assumed that in thread local storage we record a stack of pointers to CSpaces.
    
    The 'current CSpace' in thread local storage refers to the CSpace at the top of the
    stack.  $new applies to the current CSpace.
    
    When a thread is created the stack is empty.  Therefore $new will cause an assertion
    if no CSpace has been pushed onto the stack.
    */
    
    void ImportImage3(Folder* f, ceda::ConstStringZ path)
    {
        /*
        Creates a local 'child' CSpace that never runs a GC and doesn't have a mutex
        to lock because it is only intended for exclusive use by this thread.
        
        Its purpose is to
        
            1.   temporarily hide the parent CSpace by pushing itself onto the stack of
                 CSpace pointers in thread local storage.  When it falls out of scope it
                 will pop itself;
                 
            2.   allow $new to register objects into this child cspace;
            
            3.   later allow the objects to be transferred into the parent CSpace; and
            
            4.   delete any objects still registered with it when it goes out of scope.
        */
        ceda::LocalCSpace child;
        
        Image* i = $new Image;
        i->LoadFromDisk(path);
        
        {
            /*
            The LockAndTransfer declaration does the following: 
            
                 1.  Temporarily reverses 'parent' and 'child' pointers on the stack
                     of CSpace pointers in thread local storage.
                     Therefore the current CSpace becomes the parent CSpace.
                     
                 2.  The parent CSpace is locked
                 
                 3.  All objects in the child CSpace are transferred to the 
                     parent CSpace.
            */
            ceda::LockAndTransfer lock;
            
            f->Add(i);
        }
    }
    
    void Run()
    {
        ceda::TraceGroup g("gcrootExample2");

        ceda::CSpaceCreator cspace(20); // GC every 20 msec

        // Create a folder and make a root to protect from GC
        ceda::gcroot<Folder*> folder;
        {
            ceda::CSpaceLock lock;
            folder.reset($new Folder);
        }
        
        // Import image into the folder
        ImportImage3(folder.get(), "blah");
        
        Tracer() << "Waiting\n";
        Sleep(300);
    }
}

///////////////////////////////////////////////////////////////////////////////////////////////////
/*
Illustrates the error of using $new (or more generally a call to RegisterGcObject()) from within 
an implementation of VisitObjects().

Typically VisitObjects() is called by a GC thread that hasn't even set thread local storage
for the associated CSpace.

Therefore the RegisterGcObject() trips an assertion (because GetThreadPtr<CSpace>() returns NULL).
*/
namespace NoObjectRegistrationInVisitFunction
{
    $struct X isa ceda::IObject
    {
        void VisitObjects(ceda::IObjectVisitor& v) const
        {
            Tracer() << "VisitObjects() : allocating an object\n";
            $new X;
        }
    };
    
    void Run()
    {
        ceda::TraceGroup g("NoObjectRegistrationInVisitFunction");

        ceda::CSpaceCreator cspace(50);     // GC every 50 msec

        // Create an X and make it a root
        X* x;
        {
            ceda::CSpaceLock lock;
            x = $new X;
            AddGcRoot(x);
        }
        
        Tracer() << "Waiting\n";
        Sleep(100);
    }
}

///////////////////////////////////////////////////////////////////////////////////////////////////

namespace gcrootVectorExample
{
    $struct X << reflect >> isa ceda::IObject
    {
        X() { Tracer() << "X()\n"; x = 1; }
        ~X() { Tracer() << "~X()\n"; x = 0; }

        void VisitObjects( ceda::IObjectVisitor& v ) const
        {
            Tracer() << "X::VisitObjects()\n";
        }
        
        void foo() { Tracer() << "Foo\n"; cxAssert(x == 1); }
        int x;
    };
	
    void Run()
    {
        ceda::TraceGroup g("gcrootVectorExample");
        ceda::CSpaceCreator cspace(20);     // GC every 50 millsec
        std::vector<ceda::gcroot<X*>> vec;
        {
            ceda::CSpaceLock lock;
            vec.push_back( ceda::gcroot<X*>($new X) );
            vec.push_back( ceda::gcroot<X*>($new X) );
            vec.push_back( ceda::gcroot<X*>($new X) );
        }
        Sleep(100);
        for (auto& p : vec)
        {
            p->foo();
        }
        Sleep(100);    // Wait long enough for a GC
    }
}

namespace CSpaceObjectCreation
{
    void Run()
    {
        CSpaceObjectCreation1::Run();
        CSpaceObjectCreationWithSharedReadLock::Run();
        gcrootExample::Run();
        gcrootExample2::Run();
        
        // This fails if run (so it should be commented out)
        //NoObjectRegistrationInVisitFunction::Run();

        gcrootVectorExample::Run();
    }
}