Cleaning up after yourself: how and when to dispose of AutoCAD objects in .NET

A question came up recently in an internal discussion and I thought I'd share it as it proved so illuminating.

If I have an object of a type which implements IDisposable, is it good practice to explicitly dispose it (whether via the using statement or calling Dispose() explicitly)?

The quick(ish) answer is:

Yes it is, but sometimes you might choose not to as the increase in code simplicity outweighs the benefits derived from manually disposing of the objects.

So, naturally, the devil is in the detail. Let's take a look at the three scenarios where you're likely to be working with IDisposable objects inside AutoCAD:

  1. Temporary objects - such as those provided by Autodesk.AutoCAD.Geometry - which are never Database-resident
  2. Temporary objects with the potential to be database-resident but which never actually get added to a Database
  3. Database-resident objects added/accessed via a Transaction

Below follows the details on each of these categories.

Temporary objects of types not derived from DBObject

The first category of temporary objects, such as Geometry.Line, are safe to be disposed of either "manually" (by your own code) or "automatically" (by the .NET garbage collector).

Temporary objects of DBObject-derived types

The second category of temporary objects, which are of a type derived from DBObject, must be disposed of manually. It is absolutely unsafe not to dispose of objects of DBObject-derived classes from the main execution thread in AutoCAD.

Why is this the case? Firstly, the majority of the classes available through AutoCAD's .NET API are currently unmanaged, with a relatively thin wrapper exposing them to the .NET world. Inside AutoCAD, all Database-resident objects are managed by a single-threaded runtime component, AcDb (which, along with some other components, is productized as Autodesk RealDWG). A side note: if you're using ObjectARX or RealDWG from C++, don't be confused by the fact your project's C-runtime memory management is likely to be "Multi-threaded DLL", RealDWG is not thread-aware and so must be treated as single-threaded, for all intents and purposes.

And - secondly - on to the reason that automatic garbage collection is not to be trusted on DBObject-derived types: the .NET garbage collector runs on a separate, typically low-priority - unless memory is running short - thread. So if a DBObject-derived type is garbage-collected inside AutoCAD, a separate thread will essentially call into a non thread-safe component (RealDWG), which is very, very likely to cause AutoCAD to crash.

So you must always call Dispose() on temporary objects of DBObject-derived classes in your .NET code - you cannot rely on the CLR to manage your objects'  lifetimes for you.

Interestingly, this is also the reason why the F# code I posted in this prior post will not effectively leverage multiple cores (something I've just tested since getting a multi-core machine). We are using a Ray (which is a DBObject-derived class) to get intersection points with our path, and then disposing of this temporary object from an arbitrary thread (farmed off via F# Asynchronous Workflows). So this is unsafe, and won't run for long before crashing. At least now I understand why.

Database-resident, Transaction-managed objects

The third category of objects are those we use a Transaction either to add to the Database or to open for access. The good news is that - as the Transaction is aware of the objects it manages (via calls to AddNewlyCreatedDBObject() or to GetObject()), it is able to dispose of them automatically when it, itself, is disposed. So the key here is to make sure you wrap your use of Transactions in using blocks or call Dispose() on them when you're done. There is no need to explicitly dispose of the objects managed by a Transaction (unless there is a failure between the time the object is created and when it is added to the transaction via AddNewlyCreatedDBObject(), of course).

18 responses to “Cleaning up after yourself: how and when to dispose of AutoCAD objects in .NET”

  1. Hi Kean. The issue you describe with non-database-resident objects and Dispose() is actually part of a more general issue related to AutoCAD threads and the GC.

    Being a cooperative fiber mode application (e.g., lightweight threads or "fibres" that share a common thread-local storage), it is generally safe to access AutoCAD APIs from any AutoCAD fiber, but is generally unsafe to access those APIs from any other thread, such as an OS thread you create yourself via System.Threading.

    The thread which the GC's finalizer runs in is also an OS thread rather than a fibre, so in fact, even something as seemingly benign as calling acutPrintf() from that thread will fail.

    And, if one is writing their own types that implement IDisposable, they should also not attempt to access any AutoCAD API from the implementation of Dispose(), if it is being called directly by the finalizer.

  2. Hi Kean,

    I've been dealing with the MAP .NET API recently, in particular, the ClassificationManager and FeatureClassProperties etc. and I've noticed that there isn't a transaction to be seen in the Adesk samples. Also, every object seems to implements IDisposable, so what is the recommended disposal pattern for MAP, or the verticals in general.

    I've noticed funny things with memory consumption, strange crashes etc. when I be a good .NET citizen and try to dispose of anything I think is not dbase resident...just looking for clarification...

    Cheers,
    Glenn.

  3. Hi Glenn,

    Sorry - I'm not familiar with the APIs to Map 3D (although members of my team are, of course).

    I would expect AutoCAD-based applications to follow similar object disposal patterns (AutoCAD Architecture and MEP do, I believe), but clearly there are intricacies to the vertical APIs that I'm not aware of.

    If you submit your question to via the ADN website or the AutoCAD Map 3D Developer Discussion Group, I'm sure you'll get a more useful answer.

    Regards,

    Kean

  4. Hi Kean,

    thanks for another useful blog entry - a constant source of new insights.
    But how do I deal with dispose() in C++/CLI? Is it enough to call delete() on the transaction-object? Would it be even better trying to create new objects on the managed stack instead of on the managed heap (I don't even know, if this is possible with AutoCAD classes)?

    thanks, Bernd

  5. Kean Walmsley Avatar

    Hi Bernd,

    I've found this article to be a great source of knowledge on this topic (if a little in depth for this particular post).

    It states in the section "Working With Disposable Objects":

    [begin quote]
    Consider disposing of any IDisposable object instances when you are done with them. [...] The easiest way to do this in C# is with the using statement. The easiest way to do this in C++ is by allocating objects on the stack (the default).
    [end quote]

    It goes on to show "equivalent" C# and C++ code. So defining the scope of variables on the stack should work, as manual deletion of variables on the heap should, too (one assumes :-).

    Regards,

    Kean

  6. Hi Kean,

    that looks like a real comprehensive article, thanks for doing the researches. I'll have to take a closer look at it.

    cheers, Bernd

  7. Chuck Gabriel Avatar

    Kean,

    Do temporary databases such as the one used below need to be disposed?

    public static double getDwgScale(string fileName)
    {
    if (!File.Exists(fileName))
    throw new FileNotFoundException("No such file.");
    Database db = new Database(false, true);
    try
    {
    db.ReadDwgFile(fileName, System.IO.FileShare.Read, false, string.Empty);
    return db.Dimscale;
    }
    catch (Autodesk.AutoCAD.Runtime.Exception e)
    {
    Application.DocumentManager.MdiActiveDocument.Editor.WriteMessage(e.ToString());
    return 0;
    }
    }

    BTW - Thanks for all your articles. They have helped me understand a lot of concepts that aren't so clearly defined in the docs.

  8. Kean Walmsley Avatar

    Chuck,

    I would certainly dispose of them - the using() statement should help take care of it, even if you return a Database property from your function.

    Regards,

    Kean

  9. Hi Kean,

    thanks for your very interesting article.
    You write that a DBObject-derived type must be disposed of "manually" to avoid that it will be accessed by the GC thread.

    Does this generally mean that a DBObject-derived type must be accessed from AutoCAD's main thread in any case ?

    In our organization we use AutoCAD OEM 2007. We call our .NET AddIn from another application (out-of-process). We do this by .NET remoting. As all remoting calls are executed on the thread pool we are on the "wrong" thread when we enter the AddIn.

    Could you please give a recommendation how to get onto the "right" thread ?

    We also found an ADN article about that topic (ID: TS88407) where the Control.Invoke() method is mentioned.
    Here the question is: How to get a "Control"? Is there an (invisible) AutoCAD main window when we use AutoCAD via in-place-server (ActiveX control)?

    Thanks,
    Susanne

  10. Hi Kean,

    thanks for your very interesting article.
    You write that a DBObject-derived type must be disposed of "manually" to avoid that it will be accessed by the GC thread.

    Does this generally mean that a DBObject-derived type must be accessed from AutoCAD's main thread in any case?

    In our organization we use AutoCAD OEM 2007. We call our .NET AddIn from another application (out-of-process). We do this by .NET remoting. As all remoting calls are executed on the thread pool we are on the "wrong" thread when we enter the AddIn.

    Could you please give a recommendation how to get onto the "right" thread ?

    We also found an ADN article about that topic (ID: TS88407) where the Control.Invoke() method is mentioned.
    Here the question is: How to get a "Control"? Is there an (invisible) AutoCAD main window when we use AutoCAD via in-place-server (ActiveX control)?

    Thanks,
    Susanne

  11. Hi Susanne,

    I would recommend doing all access & manipulation of AutoCAD entities within your plugin inside AutoCAD: remoting is not at all supported for out-of-process access of AutoCAD entities. COM will work, but not the managed API.

    For more detailed support on this, please submit your question through ADN - they are better placed to help you with this.

    Regards,

    Kean

  12. Ok..but I don't understood the right "scenario" working with DbObjectCollection. (inherits from IList)
    I knew that .net framework calls method dispose of members of list if availables.

    Buth the cad help says
    _______________________________________________
    When a DBObjectCollection is populated by unmanaged code, the objects in the collection do not yet have managed wrappers. Neither are they (typically) database-resident. DBObjectCollection will generate a wrapper for each object if and when that object is accessed (retrieved) from the collection. If there are no retrieval requests, no wrappers are generated. When a DBObjectCollection is disposed, it looks at the set of unmanaged objects it holds, and the set of wrappers it has generated. For those objects with wrappers, it assumes the wrappers will manage the lifetime of the object, and it does nothing. For those objects without wrappers, it will delete them or close them: it takes responsibility for object lifetime.

    Therefore, you must not get into a situation where an AcDbObject, with a managed wrapper, gets stored in a DBObjectCollection without its wrapper. This can happen if the DBObject is passed into unmanaged code, the unmanaged object is stored in an AcDbVoidPtrArray (the imp-side of a DBObjectCollection), and the AcDbVoidPtrArray turned back into a DBObjectCollection and returned to managed code.

    it is important to be sure DBObjectCollections are properly (perhaps explicitly) disposed.
    _____________________________________________

    I understood this... if You iterate once on collection a wrapper is used to dispose the object.
    (first problem..and if i don't touch the list..no wrapper disposed is generated ?)
    ok..but why the help says " and it does nothing if the wrapper has been generated ? I guess it called "Dispose()"!
    I guess the contrary..
    " For those objects without wrappers, it will delete them or close them: it takes responsibility for object lifetime."
    if the wrapper doesn't exists the objects are cleared ??

    anf finally the really chaos to me..

    Therefore, you must not get into a situation where an AcDbObject, with a managed wrapper, gets stored in a DBObjectCollection without its wrapper. This can happen if the DBObject is passed into unmanaged code, the unmanaged object is stored in an AcDbVoidPtrArray (the imp-side of a DBObjectCollection), and the AcDbVoidPtrArray turned back into a DBObjectCollection and returned to managed code.

    thare are someone able to write 4 rows of examples...more clear then this help ?

  13. Giuliano -

    How is the DbObjectCollection being populated? If you're creating and populating it all with managed code then you should assume you need to manually dispose of each and every object contained.

    If the collection is somehow being returned from unmanaged code there's more risk, but it all depends on how the unmanaged code is populating the collection. If the unmanaged code that populates the array is being passed an object via its unmanaged pointer when a managed wrapper already exists, that's when problems may arise.

    I hope this helps make things a little clearer - it sounds like this a topic worthy of a blog post...

    Kean

  14. Giuliano -

    How is the DbObjectCollection being populated? If you're creating and populating it all with managed code then you should assume you need to manually dispose of each and every object contained.

    If the collection is somehow being returned from unmanaged code there's more risk, but it all depends on how the unmanaged code is populating the collection. If the unmanaged code that populates the array is being passed an object via its unmanaged pointer when a managed wrapper already exists, that's when problems may arise.

    I hope this helps make things a little clearer - it sounds like this a topic worthy of a blog post...

    Kean

  15. Hi Kean, thank You for your attention.

    ok...Sorry if I was a little bit unclear. But I haven't really yet understood some steps..

    Now I try to show You my doubt:

    I had a problem when I used
    DbObjectCollection cc = Region.CreateFromCurves(singleObjCol);

    TonyT. suggest to iterate over

    so with
    foreach(DbObject c in cc)
    {}

    and this made a wrapper for every Dbobject.
    ok ? yes...of course 🙂

    After this I perform extrusion ecc..with my regions without append one of those regions at database.

    At the end, I call Dispose of every single object
    foreach(DbObject c in cc)
    {
    c.Dispose()
    }
    cc.Dispose()

    With this code everything looks ok.
    Without c.Dispose() autocad Crash.

    I thought "cc.Dispose()" was enough..in order to set free the memory.
    ________________________________________

    You said:
    If the collection is somehow being returned from unmanaged code there's more risk, but it all depends on how the unmanaged code is populating the collection. If the unmanaged code that populates the array is being passed an object via its unmanaged pointer when a managed wrapper already exists, that's when problems may arise.

    The question is: Can I know how the api return
    the collection ? "somehow" looks like a nightmare 🙂
    (with wrapper or not in order to dispose it ?)

    What is the better way to ensure free errors with collections ?
    If I use c++ i can see the memory..but with .Net i don't know what happens behind the scenes.

    I Hope to not be very stressful but that's yet a little fog in my mind..

  16. If you're using a standard API - and not handling the population of this collection yourself, in your own unmanaged code - then this should be altogether more straightforward. Also the fact that the objects in the collection are newly created should also make things easier.

    In theory - and I haven't tried it - if you don't access anything in the collection, then you shouldn't need to dispose of anything but the collection itself. But if you create a wrapper for any item in the collection - using Tony T's code, for instance - then you will need to dispose of them (the ones for which wrappers have been created).

    At the end of the day going through each item and disposing each one will create the wrapper of one doesn't exist and dispose of it immediately, which is what I would probably do as it's relying less on trust.

    Kean

  17. What category does ObjectId fall into? Should I call Dispose on these objects when I no longer need them?

  18. No need - ObjectIds are not disposable (i.e. the class is not derived from IDisposable), so there's nothing you need to do.

    Kean

Leave a Reply to Tony Tanzillo Cancel reply

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