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.
#ifdef _MANAGED
# include <gcroot.h>
# define GCROOT(A) gcroot<A>
#else
# define GCROOT(A) intptr_t #endif
class Wrapper
{
GCROOT(System::String^) m_value;
public:
Wrapper();
~Wrapper();
void someService();
};
#include "Header.h"
Wrapper::Wrapper()
{
m_value = gcnew System::String("lucky me !");
}
Wrapper::~Wrapper()
{
}
void Wrapper::someService()
{
System::Console::WriteLine(m_value);
}
#include "Header.h"
void function()
{
Wrapper wrapper; {
Wrapper copy(wrapper); }
wrapper.someService(); }
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.
#ifdef _MANAGED
# include <gcroot.h>
# define GCROOT(A) gcroot<A>
#else
struct safe_intptr_t
{
private:
safe_intptr_t(); ~safe_intptr_t(); safe_intptr_t(const safe_intptr_t& src); void operator= (const safe_intptr_t& src); 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:
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:
class gchandle
{
private:
int m_ref;
void* m_handle;
gchandle(); gchandle(const gchandle&); void operator= (const gchandle&);
~gchandle();
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;
void reference()
{
m_ref += 1;
}
void unreference()
{
if(--m_ref == 0)
delete this;
}
}
#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):
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:
#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:
#include <gcref.h>
MANAGED_TYPEDEF(System::Array, Array);
class Sample
{
public:
gcref<Array> m_array;
Sample() { } Sample(const Sample& src) : m_array(src.m_array) { } ~Sample() { } void share(const Sample& src) { m_array = src.m_array; } bool same(const Sample& src) const { return m_array == src.m_array; } };
And the type checking is consistent, even in native.
#include <gcref.h>
MANAGED_TYPEDEF(System::Array, Array);
MANAGED_TYPEDEF(System::IO::File, File);
MANAGED_TYPEDEF(System::IO::File, Array);
gcref<Array> array;
gcref<File> file;
file = array;
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.
#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) {
sethandle(0);
}
bool operator== (_gcnull) {
return m_handle == 0;
}
#endif
};
void sample()
{
gcref<Array> array;
array = 0; array == 0; }
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.