Developing C++/CLI handle classes

Table of contents

Introduction

C++/CLI, one of the languages of the .NET Framework, is rarely used to develop large, stand-alone projects. Its main purpose is to create some assemblies for .NET interaction with the native (unmanaged) code. Accordingly, classes called handle ones, managed classes that have a pointer to the native class as a member, are widely used. Typically, such a handle class owns the corresponding native object, that is, it must delete it at the appropriate time. It is quite natural to make such a class disposable, that is, implement the System::IDisposable interface. The implementation of this interface in .NET must follow a special pattern called Basic Dispose [Cwalina]. A remarkable feature of C++/CLI is that the compiler takes on almost all the routine work of implementing this pattern, whereas in C# almost everything must be done manually.

1. Basic Dispose pattern in C++/CLI

There are two main ways to implement this pattern.

1.1. Defining a destructor and finalizer

In this case, the destructor and finalizer should be defined in the managed class, the compiler will do the rest.

public ref class X
{
    ~X() {/* ... */} // destructor
    !X() {/* ... */} // finalizer
    // ...
};

The compiler does the following:

  1. For the X class, it implements the System::IDisposable interface.
  2. In X::Dispose() provides a call to the destructor, a call to the destructor of the base class (if it exists) and a call to GC::SupressFinalize().
  3. Overrides System::Object::Finalize() where it provides a call to the finalizer and finalizers of base classes (if any).

Inheritance from System::IDisposable can be specified explicitly, but you cannot define X::Dispose() yourself.

1.2. Using stack semantics

The Basic Dispose pattern is also implemented by the compiler if the class contains a member of disposable type and it is declared using stack semantics. This means that the declaration uses the name of the type without a caret (‘^‘), and the initialization occurs in the initialization list of the constructor, and not with gcnew. Stack semantics is described in [Hogenson].

Let’s give an example:

public ref class R : System::IDisposable
{
public:
    R(/* parameters */); // constructor
    // ...
};
public ref class X
{
    R m_R; // not a R^ m_R

public:
    X(/* parameters */) // constructor
        : m_R(/* arguments */) // not a m_R = gcnew R(/* arguments */)
    {/* ... */}
    // ...
};

The compiler in this case does the following:

  1. For the X class, it implements the System::IDisposable interface.
  2. In X::Dispose() provides a call to R::Dispose() for m_R.

Finalization is determined by the appropriate R class functionality. As in the previous case, the inheritance from System::IDisposable can be specified explicitly, and you cannot define your own X::Dispose(). Of course, the class can contain other members declared using the stack semantics, and they are also provided with a call to Dispose().

2. Managed templates

And finally, another great feature of C++/CLI makes it as easy as possible to create handle classes. We are talking about managed templates. These are not generics, but real templates, as in classic C++, i.e. templates of managed classes. Instantiating such templates leads to the creation of managed classes that can be used as base classes or class members within an assembly. Managed templates are described in [Hogenson].

2.1. Smart pointers

Managed templates allow you to create classes like smart pointers that hold a pointer to the native object as a member and ensure its deletion in the destructor and finalizer. Such smart pointers can be used as base classes or members (of course, using the stack semantics) when developing handle classes that are automatically disposable.

Let’s give an example of such templates. The first is the base template, the second is intended for use as a base class and the third as a class member. These templates have a template parameter (native), designed to delete the native object. The default deleter deletes the object with the delete operator.

// native template, default deleter, T is a native class
template <typename T>
struct DefDeleter
{
    void operator()(T* p) const { delete p; }
};

// managed templates,
// smart pointers to a native object

// base template, T is a native class, D is a deleter
template <typename T, typename D>
public ref class ImplPtrBase : System::IDisposable
{
    T* m_Ptr;

    void Delete()
    {
        if (m_Ptr != nullptr)
        {
            D del;
            del(m_Ptr);
            m_Ptr = nullptr;
        }
    }

    ~ImplPtrBase() { Delete(); }
    !ImplPtrBase() { Delete(); }

protected:
    ImplPtrBase(T* p) : m_Ptr(p) {}

    T* Ptr() { return m_Ptr; }
};

// template to be used as a base class
template <typename T, typename D = DefDeleter<T> >
public ref class ImplPtr : ImplPtrBase<T, D>
{
protected:
    ImplPtr(T* p) : ImplPtrBase(p) {}

public:
    property bool IsValid
    {
        bool get() { return (ImplPtrBase::Ptr() != nullptr); }
    }
};

// template to be used as a class member
template <typename T, typename D = DefDeleter<T> >
public ref class ImplPtrM sealed : ImplPtrBase<T, D>
{
public:
    ImplPtrM(T* p) : ImplPtrBase(p) {}
    operator bool() { return ( ImplPtrBase::Ptr() != nullptr); }
    T* operator->() { return ImplPtrBase::Ptr(); }
    T* Get() { return ImplPtrBase::Ptr(); }
};

2.2. Usage example

class N // native class
{
public:
    N();
    ~N();
    void DoSomething();
    // ...
};

using NPtr = ImplPtr<N>; // base class

public ref class U : NPtr // managed handle class
{
public:
    U() : NPtr(new N()) {}
    void DoSomething() { if (IsValid) Ptr()->DoSomething(); }
    // ...
};

public ref class V // managed handle class, second version
{
    ImplPtrM<N> m_NPtr; // stack semantics

public:
    V() : m_NPtr(new N()) {}
    void DoSomething() { if (m_NPtr) m_NPtr->DoSomething(); }
    // ...
};

In these examples, the U and V classes are disposable without any additional effort; their Dispose() applies the delete operator to a pointer to N. The second version, using ImplPtrM<>, allows you to manage several native classes in the same handle class.

2.3. More complex options for finalization

Finalization is a rather problematic aspect of the work of .NET. In normal application scenarios finalizers should not be called; resource release occurs in Dispose(). But in emergency scenarios this can happen, and finalizers should work correctly.

2.3.1. Blocking finalizers

If the native class is in a DLL that is loaded and unloaded dynamically – using LoadLibrary()/FreeLibrary(), there may be a situation when, after unloading the DLL, there are objects that have references to instances of the native class. In this case, after some time the garbage collector will try to finalize them, and since the DLL is unloaded, the program is likely to crash. (A symptom is a crash a few seconds after the application closes visually.) Therefore, after unloading the DLL, the finalizers should be blocked. This can be achieved by a small modification of the basic template ImplPtrBase<>.

public ref class DllFlag
{
protected:
    static bool s_Loaded = false;

public:
    static void SetLoaded(bool loaded) { s_Loaded = loaded; }
};

template <typename T, typename D>
public ref class ImplPtrBase : DllFlag, System::IDisposable
{
    // ...
    !ImplPtrBase() { if (s_Loaded) Delete(); }
    // ...
};

After loading a DLL, call DllFlag::SetLoaded(true), and before unloading DllFlag::SetLoaded(false).

2.3.2. Using SafeHandle

The SafeHandle class implements rather complex and highly reliable finalization algorithm, see [Richter]. The ImplPtrBase<> template can be redesigned so that it uses SafeHandle. The rest of the templates do not need any changes.

using SH = System::Runtime::InteropServices::SafeHandle;
using PtrType = System::IntPtr;

template <typename T, typename D>
public ref class ImplPtrBase : SH
{
protected:
    ImplPtrBase(T* p) : SH(PtrType::Zero, true)
    {
        handle = PtrType(p);
    }

    T* Ptr() { return static_cast<T*>(handle.ToPointer()); }

    bool ReleaseHandle() override
    {
        if (!IsInvalid)
        {
            D del;
            del(Ptr());
            handle = PtrType::Zero;
        }
        return true;
    }

public:
    property bool IsInvalid
    {
        bool get() override
        {
            return (handle == PtrType::Zero);
        }
    }
};

Bibliography


Cwalina, Crzysztof and Abrams, Brad. Framework Design Guidelines, Second Edition. Addison-Wesley, 2008.


Hogenson, Gordon. ะก++/CLI: the Visual C++ Languge for .NET. APress, 2006.


Richter, Jeffrey. CLR via C#, 4 edition. Microsoft Press, 2012.