11

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:

  1. Allocates memory in the process -- In the unmanaged part of it.
  2. Declares refPtr as a pointer to a TypedReference 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 type TypedReference* would throw an exception.
  3. Implicitly casts refPtr to void* and assigns the pointer to _ptr.
  4. Sets tr as the value pointed by refPtr 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.

4

4 に答える 4

3

You can also achieve this without using unmanaged memory, by creating a "fake" type that resembles typed reference in its structure:

unsafe class Horror
{
    FakeTypedRef ftr;

    static void Handler()
    {
        Console.WriteLine("Hello void.");
    }

    public void Fix(ref Action action)
    {
        action += Handler;
        TypedReference tr = __makeref(action);
        ftr = *(FakeTypedRef*)(&tr);
    }

    public void Clear()
    {
        fixed(FakeTypedRef* ptr = &ftr)
        {
            var tr = *(TypedReference*)ptr;
            __refvalue(tr, Action) -= Handler;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    struct FakeTypedRef
    {
        public IntPtr Value;
        public IntPtr Type;
    }
}

Important Edit: I strongly advise against passing any reference around as a pointer. The GC is allowed to freely move objects on the managed heap as it sees fit, and there is no guarantee that the pointer will stay valid not even after it is returned from the method. You may not see the immediate effect of this due to debug, but you are unleashing all sorts of problems by this.

If you really need to handle it as a pointer (and there may be some legitimate reasons), you need to emit a custom CIL with a pinned reference. It could even be initialized by extracting the pointer from a TypedReference, but it guarantees that the location will not change. Pass it to a lambda method then.

于 2014-11-14T10:57:54.220 に答える
2

Yes! You can trick the compiler into creating a field of type TypedReference, by creating an iterator or lambda that uses one (or maybe an async block):

static IEnumerable<object> Sneaky(TypedReference t1)
{
    yield return TypedReference.ToObject(t1);
}

static Func<object> Lambda(TypedReference t1)
{
    return () => TypedReference.ToObject(t1);
}

Unfortunately, the CLR does not let you actually use the class. You'll get a TypeLoadException when you try.

In other words, the TypedReference and ArgIterator types are protected not just by the compiler, but by the runtime. If you want to save a copy of either one, you'll have to do so by doing an unsafe blit onto the heap or Reflection.

Note that I tried this with the .Net 4.0 C# compiler. Other compilers may be smarter.

于 2013-01-07T07:19:41.797 に答える
1

TypedReferenceはかなり決定的にロックダウンされているようです。

渡されるのを許可するのではなく、安全でない状況でのみタイプをロックダウンして安全に保つ方が簡単だったと思います。

あなたはしばらくの間1つを保持することができます...しかしあなたにスレッドがかかります:)

namespace TehHorror
{
    using System;
    using System.Threading;    
    class Horror
    {
        private ManualResetEvent waiter = null;    
        public void Fix(ref Action multicast)
        {
            waiter = new ManualResetEvent(false);
            multicast += HorrorHandler;
            if (multicast != null) multicast();
            var tr = __makeref(multicast);
            waiter.WaitOne();
            __refvalue(tr, Action) -= HorrorHandler;
        }    
        public void Clear() { waiter.Set(); }    
        private static void HorrorHandler()
        {
            Console.WriteLine("Hello from horror handler.");
        }
    }    
    class Program
    {
        static void Main()
        {
            Action a = () => Console.WriteLine("Hello from original delegate");
            var horror = new Horror();
            a.Invoke();
            Action fix = () => horror.Fix(ref a);
            fix.BeginInvoke(fix.EndInvoke, null);
            Thread.Sleep(1000);
            horror.Clear();
            a.Invoke();
        }
    }
}
于 2013-01-11T19:38:16.493 に答える
1

What are you trying to do exactly? Locals are on stack and arguments are too depending on the calling convention. Storing or returning the address of a local or argument is not good because it will get overriden. There is no way to prevent them from being overriden besides like not calling methods.

If you turn on unmanaged debugging you can use the memory debugger and register window to see what is going on.

Here is simpler to understand C example. Why does print not display the correct value. Because when the print function gets invoked its stack frame overwrites the value.

int* bad(int x, int y)
{
    int sum = x + y;
    return &sum;
};

int* bad2(int x, int y)
{
    x += y;
    return &x;
}

int _tmain(int argc, _TCHAR* argv[])
{
    int* sum1 = bad(10, 10);
    int* sum2 = bad(100, 100);
    printf("%d bad", *sum1);  // prints 200 instead of 20

    sum1 = bad2(10, 10);
    sum2 = bad2(100, 100);
    printf("%d bad", *sum1);  // prints 200 instead of 20

    return 0;
};

Cant get the clr to hold on for it longer. One thing you can do is get the variable on the stack pushed further out. Below is an example. This is all bad though :(

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Runtime.InteropServices;

namespace Bad
{
    class Program
    {
        static void Main(string[] args)
        {
            Action a = () => Console.WriteLine("test");
            Horror h = new Horror();
            h.Fix(new Big(), ref a, new Big());
            h.Clear();
            Console.WriteLine();
        }
    }
    [StructLayout(LayoutKind.Sequential, Size = 4096)]
    struct Big
    {
    }
    unsafe class Horror
    {
        void* _ptr;

        static void Handler()
        {
            Console.WriteLine("Hello world.");
        }


        public void Fix(Big big, ref Action action, Big big2)
        {
            action += Handler;
            var tr = __makeref(action);
            _ptr = (void*)&tr;
        }

        public void Clear()
        {
            var tr = *(TypedReference*)_ptr;
            __refvalue(tr, Action) -= Handler;
        }
    }
}
于 2013-01-09T04:08:22.733 に答える