Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Objective-C

gcref

5.00/5 (2 votes)
15 Nov 2018CPOL3 min read 9.2K   87  
A safe and functional hold of managed types from native c++

C++ / CLI: safe_intrptr_t and Functional gcref in Managed / Unmanaged Code

This article exposes the gcroot / intptr_t pattern for holding managed references in managed / unmanaged (native) code.

It explains the risk of this pattern, and proposes two alternatives.

Background

Managed instances can be held in unmanaged memory, thanks to gcroot class (using the managed services of GCHandle::Alloc and GCHandle::Free). Sadly, gcroot itself cannot be used in C++ native code. In such case, the proposal of replacing/hiding gcroot by intptr _t is not safe, and should not be used without many precautions.

Unsafe intptr_t

Hiding/replacing gcroot by intptr_t is not safe.

C++
// Header.h
#ifdef _MANAGED
#    include <gcroot.h>
#    define GCROOT(A)    gcroot<A>
#else
#    define GCROOT(A)    intptr_t // this is not safe
#endif

class Wrapper
{
    GCROOT(System::String^) m_value;
public:
    Wrapper();
    ~Wrapper();

    void someService();
};

// ManagedSource.cpp /cli
#include "Header.h"
Wrapper::Wrapper()
{
    m_value = gcnew System::String("lucky me !");
}

Wrapper::~Wrapper()
{
    // ~gcroot on m_value
}

void Wrapper::someService()
{
    System::Console::WriteLine(m_value);
}

// UnManagedSource.cpp not /cli
#include "Header.h"

void function()
{
     Wrapper wrapper;           // ok, gcroot m_value is defined in ManagedSource.cpp 
     {
         Wrapper copy(wrapper); // oops !
                                // default copy ctor is defined in UnManagedSource
                                // but destructor will be done in ManagedSource
                                // handle is Free
     }
     wrapper.someService();     // handle has been Free, and is no more valid. Crash
}

If any Wrapper constructor / copy constructor / assignment operator / destructor is defined in unmanaged code, this will append on intptr_t m_value, which means nothing to gcroot (i.e., GCHandle). In this example, copy constructor is made on intptr_t (copy the value), but the GCHandle is free in the destructor of copy, causing a crash in wrapper.someService.

The only way to prevent this to append is to define all these methods in C++/CLI files, and prevent any modification of the handle (i.e., intptr_t) in unmanaged source. A big deal for human people, considering the risks of C++ default and/or inline implementations.

safe_intptr_t

The purpose of safe_intptr_t is to prevent all these mistakes from happening, thanks to our best friend: the compiler.

C++
// Header.h
#ifdef _MANAGED
#   include <gcroot.h>
#   define GCROOT(A)    gcroot<A>
#else
    struct safe_intptr_t 
    {
    private:
        // keep all members private
        safe_intptr_t();                               // do not implement
        ~safe_intptr_t();                              // do not implement
        safe_intptr_t(const safe_intptr_t& src);       // do not implement
        void operator= (const safe_intptr_t& src);     // do not implement
        intptr_t ptr; };
#   define GCROOT(A)    safe_intptr_t
#endif

If any constructor / copy constructor / destructor / assignment operator definition appends in C++ native code, the compiler will cause an error. With the previous Wrapper implementation, and using the safe_intptr_t, this compiler error happens:

C++
error C2248: 'safe_intptr_t' : cannot access private member declared in class 'safe_intptr_t'
     see declaration of 'safe_intptr_t::safe_intptr_t'
     This diagnostic occurred in the compiler generated function 'Wrapper::Wrapper(const Wrapper &)'

This should be explicit enough, for you to declare and implement Wrapper::Wrapper(const Wrapper&) in managed source code.

gcref: A Functional gcroot in Unmanaged Code

If safe_intptr_t offers checking at compile time to prevent any wrong implementation, which is quite nice, it offers nothing at runtime. As long as the managed target is not explicitly used, it would be nice to have simple functionality on the managed reference such as empty constructor, copy constructor, destructor, comparison and assignment operators.

The gcref is designed to offer the same services gcroot does in managed code, plus some services in unmanaged code. This can be achieved by using an indirection between the gcref class, and the managed GCHandle. Furthermore, one GCHandle is shared between multiple gcref instances, avoiding several handle allocations (compared to gcroot implementation). This implementation is based on the shared gchandle class:

C++
// gcref.h
class gchandle
{
private:
    int      m_ref;
    void*    m_handle;

    gchandle(); // do not implement
    gchandle(const gchandle&); // do not implement
    void operator= (const gchandle&); // do not implement

    ~gchandle(); // implemented in gcref.cpp

public:

#ifdef _MANAGED
    typedef System::Runtime::InteropServices::GCHandle GCHandle;

    gchandle(System::Object^ o)
    : m_ref(0)
    {
        ASSERT(o != nullptr);
        m_handle = GCHandle::operator System::IntPtr( GCHandle::Alloc(o) ).ToPointer();
    }
    System::Object^ Target () const
    {
        return GCHandle::operator GCHandle(System::IntPtr(m_handle)).Target;
    }
#endif

    bool same(const gchandle& src) const; // implemented in gcref.cpp

    void reference()
    {
        m_ref += 1;
    }
    void unreference()
    {
        if(--m_ref == 0)
            delete this;
    }
}

// gcref.cpp /cli
#include "gcref.h"

gchandle::~gchandle()
{
    GCHandle g = GCHandle::operator GCHandle(System::IntPtr(m_handle));
    g.Free();
}

bool gchandle::same(const gchandle& src) const
{
    System::Object^ a = this->Target();
    System::Object^ b = src.Target();
    return a == b;
}

gchandle is not designed to be used as is, but through the gcref template class (the gcroot like class):

C++
// gcref.h
template <typename T>
class gcref
{
    gchandle* m_handle;

    void setHandle(gchandle* handle)
    {
        if(handle)
            handle->reference();
        if(m_handle)
            m_handle->unreference();
        m_handle = handle;
    }

public:
    gcref() : m_handle(0)
    {
    }
    gcref(const gcref& ref) : m_handle(0)
    {
        setHandle(ref.m_handle);
    }
    ~gcref()
    {
        setHandle(0);
    }
    bool operator == (gcref& r) const
    {
        return m_handle==r.m_handle || m_handle && r.m_handle && m_handle->same(*r.m_handle);
    }
    bool operator != (gcref& r) const
    {
        return ! operator==(r);
    }
    void operator = (const gcref& r)
    {
        setHandle(r.m_handle);
    }
#ifndef _MANAGED
    operator bool () const
    {
        return m_handle != 0;
    }
#else
    gcref(T^ t) : m_handle(0)
    {
        if(t != nullptr)
            setHandle(new gchandle(t));
    }
    void operator= (T^ t)
    {
        setHandle(t==nullptr ? 0 : new gchandle(t));
    }
    operator T^ () const
    {
        return m_handle ? static_cast<T^> (m_handle->Target()) : nullptr;
    }
    T^ operator -> () const
    {
        return static_cast<T^> (m_handle->Target());
    }
    bool operator== (T^ t) const
    {
        return m_handle ? m_handle->Target()==t : t==nullptr;
    }
    bool operator!= (T^ t) const
    {
        return ! operator==(t);
    }
#endif
};

The gcref class can be used as is in managed code. Sadly, this is not straightforward on the native side. The managed type is not recognised, and causes error at compile time. One workaround is to typedef the managed type. Furthermore, a safe type must be created in the native part to guarantee the type checking at compile time:

C++
// gcref.h
#ifdef _MANAGED
#     define MANAGED_TYPEDEF(A,B)     typedef A B
#else
#     define MANAGED_TYPEDEF(A,B)     typedef struct __managed##B { char* unused; } *B
#endif

VoilĂ ! The gcref class is ready to work both in managed and unmanaged code:

C++
// Sample.h

#include <gcref.h> 

MANAGED_TYPEDEF(System::Array, Array);

class Sample
{
public:
     gcref<Array> m_array;

     // All can be inline because implementation is valid both in managed and native sides

     Sample() { }                                                          // empty ctor on gcref
     Sample(const Sample& src) : m_array(src.m_array) { }                  // copy ctor on gcref
     ~Sample() { }                                                         // dtor on gcref
     void share(const Sample& src) { m_array = src.m_array; }              // assignment on gcref
     bool same(const Sample& src) const { return m_array == src.m_array; } // comparison on gcref
};

And the type checking is consistent, even in native.

C++
// Sample.h not /cli

#include <gcref.h> 

MANAGED_TYPEDEF(System::Array, Array);
MANAGED_TYPEDEF(System::IO::File, File);
MANAGED_TYPEDEF(System::IO::File, Array); // error C2011 at compile time 

gcref<Array> array;
gcref<File> file;
file = array; // error C2679 at compile time

null Pointer

One last thing that would be nice to have the null pointer working on native side, with both comparison and assignment.

This is achieved by designing a gcnull struct for this single purpose.

C++
// gcref.h
#ifdef _MANAGED
#   define MANAGED_TYPEDEF(A,B) typedef A B
#else
#   define MANAGED_TYPEDEF(A,B) struct __managed##B { char* unused; } *B
    typedef struct __gcnull
    {
    private:
        char* unused;
        __gcnull();
        __gcnull(const _NullRep& src);
    } * _gcnull;
#endif

class gcref
{
public:
#ifndef _MANAGED
     void operator= (_gcnull)  // 0 is promoted to _gcnull by the compiler
     {
          sethandle(0);
     }
     bool operator== (_gcnull) // 0 is promoted to _gcnull by the compiler
     {
          return m_handle == 0;
     }
#endif
};

// Sample.h in unmanaged source
void sample()
{
     gcref<Array> array;
     array = 0;                // OK : 0 is promoted to _gcnull by the compiler
     array == 0;               // idem
}

More Functions

Some methods could be added to the gchandle class (then the gcref class), like access to the System.Object.Equals or System.Object.GetHashCode to extend the functionality of gcref in the native side.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)