Automatic closing of AutoCAD objects with ObjectARX SmartPointers

Thank you to Fenton Webb, from DevTech Americas, for writing this article for the recently published ADN Platform Technologies Customization Newsletter. This article also talks about the new AcDbSmartObjectPointer class referenced in this overview of the new APIs in AutoCAD 2009.

Those of us who regular create ObjectARX code to manipulate the AutoCAD drawing database are fully aware of the mechanism for opening an object for read (to simply access data held inside it) or write (to update it with new data). Oh and I almost forgot - followed by a call to close() when you are done.

But here lies a very common problem illustrated by that last sentence; the problems start when you accidentally forget to close an object once you are finished with it. AutoCAD follows a strict set of rules which allows the checking-in/out of data inside of AutoCAD and these rules must be adhered to. If not, then AutoCAD will abort in order to do its best to save the previous valid state of the database.

"You must be very careful to close your objects when you are finished with them". It's very easy for me to say that, but even I, the person shaking his finger saying those infamous words will fail to remember my own advice time and time again, this is why using ObjectARX SmartPointers is a MUST.

So let's look at this thing in ObjectARX called SmartPointers.

What are they? First take a look at the MSDN article on "Template Classes" as this explains the basic concept. Leading on from that article, and now in my own words, ObjectARX SmartPointers are C++ template classes which wrap an underlying AutoCAD AcDbObject derived class pointer, and simply provides automatic closure of that pointer, if valid, on destruction of the ObjectARX SmartPointer class (so the end of a function or closing brace "}").

A question that often arises is on the usage of this class, in particular the way to access the member functions. The template class itself has been implemented so that if you reference a member function with the dot "." operator

line.openStatus()

then, you reference the ObjectARX SmartPointer specific functions. If you reference a member function with the arrow "->" operator

line->setStartPoint()

Then, because the arrow operator has been overridden to return the underlying AcDbObject pointer, you simply reference the underlying AcDbObject derived class, in this case the AcDbLine::setStartPoint().

So how do we use them then…? Let's start by showing old ObjectARX code which adds an AcDbLine to the Current Space using open and close.

// create a new line

AcDbLine *line = new AcDbLine();

// set the properties for it

line->setStartPoint(AcGePoint3d(10,10,0));

line->setEndPoint(AcGePoint3d(20,30,0));

// now add it to the current space

AcDbBlockTableRecord *curSpace = NULL;

// open the current space for write

Acad::ErrorStatus es =

  acdbOpenObject(

    (AcDbBlockTableRecord *&)curSpace,

    curDoc()->database()->currentSpaceId(),

    AcDb::kForWrite

  );

// if ok      

if (es == Acad::eOk)

{

  // add it to the space

  es = curSpace->appendAcDbEntity(line);

  // check that everything was ok

  if (es != Acad::eOk)

  {

    delete line;

    return;

  }

  // now close everything

  line->close();

  curSpace->close();

}

It's the 2 close statements at the end which are, first of all, very easy to forget to put in, but also notice they return just before which indicates a very rare failure, but just as importantly (and erroneously) bypasses the close of curSpace.

This is where ObjectARX SmartPointers not only provide automatic closure and cleanup but also peace of mind…

Let's take a look at the same code, but this time using ObjectARX SmartPointers.

// create a new line

AcDbObjectPointer<AcDbLine> line;

line.create();

// set the properties for it

line->setStartPoint(AcGePoint3d(10,10,0));

line->setEndPoint(AcGePoint3d(20,30,0));

// now add it to the current space

AcDbBlockTableRecordPointer

  curSpace(

  curDoc()->database()->currentSpaceId(),

    AcDb::kForWrite

  );

// if ok

if (curSpace.openStatus() == Acad::eOk)

{

  Acad::ErrorStatus es =

    curSpace->appendAcDbEntity(line);

  // check that everything was ok

  if (es != Acad::eOk)

  {

    // no need for a delete as the smartpointer does this for us

    return;

  }

}

// everything will be closed automatically for us

Not only is this ObjectARX code "close" safe, it is also memory leak-safe. Also, look how much tidier it is. Much more friendly in my opinion!

Here's some more SmartPointer code which selects an Entity on screen and opens it for read, just as an example.

ads_name ename;

ads_point pt;

// pick an entity to check

int res = acedEntSel (_T("\nPick a Line : "), ename, pt);

// if the user didn't cancel

if (res == RTNORM)

{

  AcDbObjectId objId;

  // convert the ename to an object id

  acdbGetObjectId (objId, ename);

  // open the entity for read

  AcDbObjectPointer<AcDbLine>ent (objId, AcDb::kForRead);

  // if ok

  if (ent.openStatus () == Acad::eOk)

  {

    AcGePoint3d startPnt;

    ent->startPoint(startPnt);

  // do something

  }

}

But what if you have reams and reams of existing code using old-style open and close, and you want to migrate to ObjectARX Smart Pointers with the least amount of effort? Well, we've tried to make it easy for you. Since ObjectARX 2007, in dbobjptr.h simply uncomment the #define DBOBJPTR_EXPOSE_PTR_REF and now life should be easy! (Well, with one exception - see **NOTE below).

Here's the converted version of the original code we used at the beginning, converting to use ObjectARX SmartPointers couldn't be easier (I've highlighted the changes in bold).

// create a new line

AcDbObjectPointer<AcDbLine> line = new AcDbLine();

// set the properties for it

line->setStartPoint(AcGePoint3d(10,10,0));

line->setEndPoint(AcGePoint3d(20,30,0));

// now add it to the current space

AcDbBlockTableRecordPointer curSpace = NULL;

// open the current space for write

Acad::ErrorStatus es =

  acdbOpenObject(

  (AcDbBlockTableRecord *&)curSpace,

    curDoc()->database()->currentSpaceId(),

    AcDb::kForWrite

  );

// if ok

if (es == Acad::eOk)

{

  // add it to the space

  es = curSpace->appendAcDbEntity(line);

  // check that everything was ok

  if (es != Acad::eOk)

  {

    delete line;

    return;

  }

  // now close everything

  line->close();

  curSpace->close();

}

Notice that I didn't bother to remove the two close() calls at the end, there's no need. If you close them by hand, or forget, it's all good with ObjectARX SmartPointers.

**NOTE: So, in order to get the acdbOpenObject to accept the same code as before, in dbobjptr.h, at line 467 (ObjectARX 2009 SDK), there is an assert which needs to be omitted; either #define NDEBUG or I recommend that you simply change the assert to be enclosed by the #ifndef DBOBJPTR_EXPOSE_PTR_REF

AcDbObjectPointerBase<T_OBJECT>::object(){

  #ifndef DBOBJPTR_EXPOSE_PTR_REF

    assert(m_status == Acad::eOk);

  #endif // DBOBJPTR_EXPOSE_PTR_REF

Last but not the least is the new AcDbSmartObjectPointer template class in ObjectARX 2009, defined in the header file dbobjptr2.h.

This new template class works in the same way as AcDbObjectPointer template class except that it works by NOT opening an object at all if its open state is already what was requested, or even closing an object multiple times before opening in the desired manner. It merely hands you the already opened object pointer for your use. This means that it is much more efficient and also much more powerful in its usage. It also treats kForNotify and kForRead in the same manner, which is effectively kForRead.

One feature of this new SmartPointer class that I'd like to talk about is the ability to multiply open an object for write, from different places, at the same time, a bit like a Transaction can – this is extremely powerful when you think about it.

At the same time though, I find thinking about the power that this can provide can start generating some other complex thoughts and scenarios that maybe we should be cautious of; the bottom line is that you should be very careful about multiply opening an object for write no matter how good the class that controls it.

An example of where this type of functionality really might be useful to us developers is in say an Object Reactor callback. Quite often you might want to modify the current object's state but of course you can't because it is already open for notify. Using this new SmartPointer class it makes it possible to modify the object as you see fit in this context, but be careful to handle the recursive object modified notifications that will be fired by doing this.

All in all a very exciting new addition to the ObjectARX API, make sure you check it out.

11 responses to “Automatic closing of AutoCAD objects with ObjectARX SmartPointers”

  1. Fernando Malard Avatar
    Fernando Malard

    Great Kean,

    One concern.
    The smart pointer close the object pointer as soon as its scope is finished. This behavior is the same as the previous smart pointer class. One problem that still concerns me is when you need to pass a pointer to other functions. In this case as soon as it leaves the function scope it will be closed and the pointer will be invalid outisde the function scope.

    I think we will need to keep these smart pointers as class members to allow it to safely travel from class functions to funcions. If you need to pass this pointer from one class to another it will need to be a global variable.

    Is there a "smarter" way to handle these cross scope smart pointers to a safe roundtrip ?

    Best Regards,
    Fernando.

  2. Fernando,

    You might be over-thinking the problem of scope. I use smart pointers all the time and I can pass the object pointer down to a function no problem. I call the function from within the scope that created the smart pointer and all is well. Going into a new function does not end the scope of defined variables on the stack. The new items are added to the stack and popped off when the function returns.

  3. Fernando Malard Avatar
    Fernando Malard

    Sorry, I did not explain this clear enough.

    The situation is when you need a function which will open an object for you and return this pointer through its return value. Something like this:

    AcDbEntity* getSmallestHeightPanel()
    {
    AcDbEntity* pEnt = NULL;
    ...
    acdbOpenObject(idEnt,pEnt,AcDb::kForRead);
    ...
    return pEnt;
    }

    If you use a smart pointer in this case it will be out of scope when returned by your function.

    In this case you will need to pass the smart pointer variable by reference so its scope is outside the function's scope.

    This type of function syntax (where the pointer is the returned value) is very convenient in some cases.

    Hope you get the idea now.
    Regards,
    Fernando Malard.

  4. Fernando,

    I would suggest that if you wish to return a pointer from a function, then you wouldn't want to use a SmartPointer to open that object in the first place. SmartPointers are a convenience to automatically close objects - something you want to avoid here.

    That said, you might want to use a new SmartPointer to receive the return value of the function:

    AcDbSmartObjectPointer panel(getSmallestHeightPanel()->objectId(), AcDb::kForRead);

    This would not actually reopen the panel entity, assuming it was already open for read.

    Regards,

    Kean

  5. Fernando Malard Avatar
    Fernando Malard

    I see.
    So the smart pointer actually checks for the current open state instead of using some local variable to control the opening status.

    Interesting. This way is easier to mix with the traditional open/close approach.

    This improvement in 2009 is only related to the template class or are there some Core modifications to allow this? Can we use this template back to ObjectARX 2007 and 2008?

    Regards.

  6. Kean Walmsley Avatar

    If you check dbobjptr2.h in ObjectARX 2009, you'll see a few internal-use function declarations, accessAcDbObjectForRead() and accessAcDbObjectForWrite(), which are exports from acdb17.dll.

    These exports appear to be there for 2008 (I didn't check any further back than that), but I wouldn't recommend attempting to use this header (and therefore these classes) in versions before 2009: object open/close is a risky area to play around with, and there may well have been internal changes made when getting this API ready for external consumption.

    Regards,

    Kean

  7. Hi Fernando

    a very interesting Use case... So using the DBOBJPTR_EXPOSE_PTR_REF define and with the changes I advised you make in dbobjptr.h your scenario is neatly dealt with. Here is what I mean...

    So without any changes to your existing routine getSmallestHeightPanel(), simply change the calling routine from (and I'm guessing)

    AcDbEntity *ent = getSmallestHeightPanel();

    to look like this...

    AcDbObjectPointer<acdbentity> ent = getSmallestHeightPanel();

    Now the returned entity is closed automatically for all return conditions, and you can sleep well at night! 🙂

    Remember that you don't need to change your code to remove the ->close() calls because they are still valid.

    I hope this helps.

    Cheers
    Fenton

  8. Hi ,

    I am having the dwg file in AutoCAD 2011 ,need to read all the entities from the drawing file.
    The following is the code, I am getting ADS request error when calling the pBlockTableRecord->close() method

    ptrOldDatabase = acdbHostApplicationServices()->workingDatabase();
    es = ptrOldDatabase->getSymbolTable(pBlockTable, AcDb::kForRead);
    if ( Acad::eOk != es )
    {
    acedAlert(ACRX_T("Couldn't open current drawing database"));
    pBlockTable->close();
    return;
    }
    es = pBlockTable->newIterator(pIteratorBlk);

    if ( Acad::eOk != es )
    {
    pBlockTable->close();
    acedAlert(ACRX_T("Couldn't open current drawing database"));
    return;
    }

    for (pIteratorBlk->start(); !pIteratorBlk->done(); pIteratorBlk->step())
    {
    AcDbBlockTableRecord*pBlockTableRecord = NULL;
    pIteratorBlk->getRecord(pBlockTableRecord, AcDb::kForRead);

    pBlockTableRecord->getName(pName);
    if ( _tcsncmp(pName,ACRX_T("*Paper_Space"),12) && _tcsncmp(pName,ACRX_T("*Model_Space"),12))
    {

    try
    {
    pBlockTableRecord->close();
    }
    catch(...)
    {

    }
    continue;
    }
    }

    Can you please help me in this regard.

    Thanks and Regards,
    Chandu.

  9. Kean Walmsley Avatar
    Kean Walmsley

    Chandu,

    This isn't a forum for support.

    Your comment doesn't appear to relate to this post, so please submit your question to the ADN team, if you're a member, or otherwise the ObjectARX Discussion Group.

    Kean

  10. Hi there,
    Hi Kean Walmsley and Fernando Malard, and other experts

    Sorry for my bad English, but...

    I have a question, after we create a custom entity in Cad drawing by ObjectArx, how to do for editing this custom entity (we can easily edit it in AutoCad or by writing code in ObjectArx, and how to do this)

    Because i successfully created a ObjectArx custom entity, but i can't edit it

    Thanks in advance for your help..!

  11. You would either implement your own commands that manipulate the properties, or you'd expose your object properties in a way that can be understood by core AutoCAD property editing (i.e. via COM or a mechanism called non-COM properties, aka ubiquity).

    Kean

Leave a Reply to chandu Cancel reply

Your email address will not be published. Required fields are marked *