I want to premise that this question's purpose is checking if there's at least one way, even if through the most unsafe hack, to keep a reference to a non-blittable value type. I am aware that such a design type is comparable to a crime; I wouldn't use it in any practical case, other than learning. So please accept reading heretic unsafe code for now.
We know that a reference to a blittable type can be stored and increased this way:
unsafe class Foo
{
void* _ptr;
public void Fix(ref int value)
{
fixed (void* ptr = &value) _ptr = ptr;
}
public void Increment()
{
var pointer = (int*) _ptr;
(*pointer)++;
}
}
In terms of safety, the above class is comparable to a jump in the void (no pun intended), but it works, as already mentioned here. If a variable allocated on the stack is passed to it and then the caller method's scope terminates, you're likely to run into a bug or an explicit access violation error. However, if you execute a program like this:
static class Program
{
static int _fieldValue = 42;
public static void Main(string[] args)
{
var foo = new Foo();
foo.Fix(ref _fieldValue);
foo.Increment();
}
}
The class won't be disposed until the relative application domain is unloaded, and so applies for the field. I honestly don't know if fields in the high-frequency heap can be reallocated but I personally think not. But let's put aside safety even more now (if even possible). After reading this and this questions I was wondering if there was a way to create a similar approach for non-blittable static types, so I made this program, which actually works. Read the comments to see what it does.
static class Program
{
static Action _event;
public static void Main(string[] args)
{
MakerefTest(ref _event);
//The invocation list is empty again
var isEmpty = _event == null;
}
static void MakerefTest(ref Action multicast)
{
Action handler = () => Console.WriteLine("Hello world.");
//Assigning a handler to the delegate
multicast += handler;
//Executing the delegate's invocation list successfully
if (multicast != null) multicast();
//Encapsulating the reference in a TypedReference
var tr = __makeref(multicast);
//Removing the handler
__refvalue(tr, Action) -= handler;
}
}
The actual problem/opportunity:
We know that the compiler won't let us store a value passed by ref, but the __makeref
keyword, as much undocumented and unadvised, offers the possibility of encapsulating and restoring a reference to blittable types. However, the return value of __makeref
, TypedReference
, is well protected. You can't store it in a field, you can't box it, you can't create an array of it, you can't use it in anonymous methods or lambdas. All that I managed to do was modifying the above code as follows:
static void* _ptr;
static void MakerefTest(ref Action multicast)
{
Action handler = () => Console.WriteLine("Hello world.");
multicast += handler;
if (multicast != null) multicast();
var tr = __makeref(multicast);
//Storing the address of the TypedReference (which is on the stack!)
//inside of _ptr;
_ptr = (void*) &tr;
//Getting the TypedReference back from the pointer:
var restoredTr = *(TypedReference*) _ptr;
__refvalue(restoredTr, Action) -= handler;
}
The above code works just as well and looks even worse than before but for the sake of knowledge, I wanted to do more with it, so I wrote the following:
unsafe class Horror
{
void* _ptr;
static void Handler()
{
Console.WriteLine("Hello world.");
}
public void Fix(ref Action action)
{
action += Handler;
var tr = __makeref(action);
_ptr = (void*) &tr;
}
public void Clear()
{
var tr = *(TypedReference*) _ptr;
__refvalue(tr, Action) -= Handler;
}
}
The Horror
class is a combination of the Foo
class and the above method, but as you'll surely notice, it has one big problem. In the method Fix
, the TypedReference
tr
is declared, its address is copied inside of the generic pointer _ptr
, then the method ends and tr
no longer exists. When the Clear
method is called, the "new" tr
is corrupted because _ptr
points to an area of the stack which is no longer a TypedReference
. So here comes the question:
Is there any way to fool the compiler into keeping a TypedReference
instance alive for an undetermined amount of time?
Any way to achieve the desired result will be considered good, even if it involves ugly, unsafe, slow code. A class implementing the following interface would be ideal:
interface IRefStorage<T> : IDisposable
{
void Store(ref T value);
//IDisposable.Dispose should release the reference
}
Please don't judge the question as generic discussion because its purpose is to see if, after all, there is a way to store references to blittable types, as wicked as it may be.
One last remark, I'm aware of the possibilities to bind fields through FieldInfo
, but it seemed to me that the latter method didn't support types deriving from Delegate
very much.
A possible solution (bounty result)
I would've marked AbdElRaheim's answer as chosen as soon as he edited his post to include the solution which he provided in his comment, but I suppose it wasn't very clear. Either way, among the techniques he provided, the one summed up in the following class (which I modified slightly) seemed the most "reliable" (ironic to use that term, since we're talking about exploiting a hack):
unsafe class Horror : IDisposable
{
void* _ptr;
static void Handler()
{
Console.WriteLine("Hello world.");
}
public void Fix(ref Action action)
{
action += Handler;
TypedReference tr = __makeref(action);
var mem = Marshal.AllocHGlobal(sizeof (TypedReference)); //magic
var refPtr = (TypedReference*) mem.ToPointer();
_ptr = refPtr;
*refPtr = tr;
}
public void Dispose()
{
var tr = *(TypedReference*)_ptr;
__refvalue(tr, Action) -= Handler;
Marshal.FreeHGlobal((IntPtr)_ptr);
}
}
What Fix
does is, starting from the line marked as "magic" in the comment:
- Allocates memory in the process -- In the unmanaged part of it.
- Declares
refPtr
as a pointer to aTypedReference
and sets it value to the pointer of the memory region allocated above. This is done, instead of using_ptr
directly, because a field with typeTypedReference*
would throw an exception. - Implicitly casts
refPtr
tovoid*
and assigns the pointer to_ptr
. - Sets
tr
as the value pointed byrefPtr
and consequently_ptr
.
He also offered another solution, the one he originally wrote as an answer, but it seemed less reliable than the one above. On the other hand, there was also another solution provided by Peter Wishart, but it required accurate synchronization and each Horror
instance would've "wasted" a thread. I'll take the chance to repeat that the above approach is in no way intended for real world usage, it was just an academic question. I hope it will be helpful for anyone reading this question.