如何在非托管内存中实例化C#类?(可能的?)

问题描述:

更新:现在有一个可以接受的答案,有效".您永远不要,永远,永远永远使用它.每个.

UPDATE: There is now an accepted answer that "works". You should never, ever, ever, ever use it. Ever.

首先,我要说我是一名游戏开发人员来开始我的问题.有一个与性能相关的正当理由(即使非常不寻常),也是要这样做的原因.

First let me preface my question by stating that I'm a game developer. There's a legitimate - if highly unusual - performance-related reason for wanting to do this.

说我有一个像这样的C#类:

Say I have a C# class like this:

class Foo
{
    public int a, b, c;
    public void MyMethod(int d) { a = d; b = d; c = a + b; }
}

没什么好看的.请注意,它是仅包含值类型的引用类型.

Nothing fancy. Note that it is a reference type that contains only value types.

在托管代码中,我想要这样的东西:

In managed code I'd like to have something like this:

Foo foo;
foo = Voodoo.NewInUnmanagedMemory<Foo>(); // <- ???
foo.MyMethod(1);

函数 NewInUnmanagedMemory 是什么样的?如果不能在C#中完成,可以在IL中完成吗?(或者也许是C ++/CLI?)

What would the function NewInUnmanagedMemory look like? If it can't be done in C#, could it be done in IL? (Or maybe C++/CLI?)

基本上:有没有一种方法-无论多么hacky-都可以将一些完全任意的指针转换为对象引用.而且-除了使CLR爆炸之外-该死的后果.

Basically: Is there a way - no matter how hacky - to turn some totally arbitrary pointer into an object reference. And - short of making the CLR explode - damn the consequences.

(提出我问题的另一种方法是:我想为C#实现自定义分配器")

(Another way to put my question is: "I want to implement a custom allocator for C#")

这导致了后续问题:当垃圾收集器面对指向托管内存之外的引用时,它会做什么(特定于实现,如果需要的话)?

This leads to the follow-up question: What does the garbage collector do (implementation-specific, if need be) when faced with a reference that points outside of managed memory?

与此相关的是,如果 Foo 将引用作为成员字段会发生什么?如果它指向托管内存怎么办?如果它仅指向非托管内存中分配的其他对象怎么办?

And, related to that, what would happen if Foo had a reference as a member field? What if it pointed at managed memory? What if it only ever pointed at other objects allocated in unmanaged memory?

最后,如果不可能的话:为什么?

Finally, if this is impossible: Why?

更新:这是到目前为止的遗漏":

Update: Here are the "missing pieces" so far:

#1:如何将 IntPtr 转换为对象引用?尽管IL不可验证,但它可能是可行的(请参阅注释).到目前为止,我还没有运气.该框架似乎非常谨慎,以防止发生这种情况.

#1: How to convert an IntPtr to an object reference? It might be possible though unverifiable IL (see comments). So far I've had no luck with this. The framework seems to be extremely careful to prevent this from happening.

(在运行时能够获取不可复制托管类型的大小和布局信息也很不错.同样,框架尝试使这一点变得不可能.)

(It would also be nice to be able to get the size and layout information for non-blittable managed types at runtime. Again, the framework tries to make this impossible.)

#2:假设一个问题可以解决-当GC遇到指向GC堆外部的对象引用时,GC会做什么?它会崩溃吗?Anton Tykhyy,在他的回答中,他猜想会的.考虑到该框架在预防#1方面的谨慎程度,看来确实有可能.证实这一点的事情会很好.

#2: Assuming problem one can be solved - what does the GC do when it encounters an object reference that points outside of the GC heap? Does it crash? Anton Tykhyy, in his answer, guesses that it will. Given how careful the framework is to prevent #1, it does seem likely. Something that confirms this would be nice.

(或者,对象引用可能指向GC堆中的固定内存.这会有所作为吗?)

(Alternatively the object reference could point to pinned memory inside the GC heap. Would that make a difference?)

基于此,我倾向于认为这种想法是不可能的,或者至少是不值得的.但是我很想得到一个涉及#1或#2或两者的技术细节的答案.

Based on this, I'm inclined to think that this idea for a hack is impossible - or at least not worth the effort. But I'd be interested to get an answer that goes into the technical details of #1 or #2 or both.

我一直在尝试在非托管内存中创建类.有可能但有一个我目前无法解决的问题-您无法将对象分配给引用类型字段 -请参阅底部的编辑内容,因此您可以在您的自定义类中只有结构字段.这很邪恶:

I have been experimenting creating classes in unmanaged memory. It is possible but has a problem I am currently unable to solve - you can't assign objects to reference-type fields -see edit at the bottom-, so you can have only structure fields in your custom class. This is evil:

using System;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

public class Voodoo<T> where T : class
{
    static readonly IntPtr tptr;
    static readonly int tsize;
    static readonly byte[] zero;

    public static T NewInUnmanagedMemory()
    {
        IntPtr handle = Marshal.AllocHGlobal(tsize);
        Marshal.Copy(zero, 0, handle, tsize);
        IntPtr ptr = handle+4;
        Marshal.WriteIntPtr(ptr, tptr);
        return GetO(ptr);
    }

    public static void FreeUnmanagedInstance(T obj)
    {
        IntPtr ptr = GetPtr(obj);
        IntPtr handle = ptr-4;
        Marshal.FreeHGlobal(handle);
    }

    delegate T GetO_d(IntPtr ptr);
    static readonly GetO_d GetO;
    delegate IntPtr GetPtr_d(T obj);
    static readonly GetPtr_d GetPtr;
    static Voodoo()
    {
        Type t = typeof(T);
        tptr = t.TypeHandle.Value;
        tsize = Marshal.ReadInt32(tptr, 4);
        zero = new byte[tsize];

        DynamicMethod m = new DynamicMethod("GetO", typeof(T), new[]{typeof(IntPtr)}, typeof(Voodoo<T>), true);
        var il = m.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ret);
        GetO = m.CreateDelegate(typeof(GetO_d)) as GetO_d;

        m = new DynamicMethod("GetPtr", typeof(IntPtr), new[]{typeof(T)}, typeof(Voodoo<T>), true);
        il = m.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ret);
        GetPtr = m.CreateDelegate(typeof(GetPtr_d)) as GetPtr_d;
    }
}

如果您担心内存泄漏,请在完成类操作后始终调用FreeUnmanagedInstance.如果您想要更复杂的解决方案,可以尝试以下方法:

If you care about memory leak, you should always call FreeUnmanagedInstance when you are done with your class. If you want more complex solution, you can try this:

using System;
using System.Reflection.Emit;
using System.Runtime.InteropServices;


public class ObjectHandle<T> : IDisposable where T : class
{
    bool freed;
    readonly IntPtr handle;
    readonly T value;
    readonly IntPtr tptr;

    public ObjectHandle() : this(typeof(T))
    {

    }

    public ObjectHandle(Type t)
    {
        tptr = t.TypeHandle.Value;
        int size = Marshal.ReadInt32(tptr, 4);//base instance size
        handle = Marshal.AllocHGlobal(size);
        byte[] zero = new byte[size];
        Marshal.Copy(zero, 0, handle, size);//zero memory
        IntPtr ptr = handle+4;
        Marshal.WriteIntPtr(ptr, tptr);//write type ptr
        value = GetO(ptr);//convert to reference
    }

    public T Value{
        get{
            return value;
        }
    }

    public bool Valid{
        get{
            return Marshal.ReadIntPtr(handle, 4) == tptr;
        }
    }

    public void Dispose()
    {
        if(!freed)
        {
            Marshal.FreeHGlobal(handle);
            freed = true;
            GC.SuppressFinalize(this);
        }
    }

    ~ObjectHandle()
    {
        Dispose();
    }

    delegate T GetO_d(IntPtr ptr);
    static readonly GetO_d GetO;
    static ObjectHandle()
    {
        DynamicMethod m = new DynamicMethod("GetO", typeof(T), new[]{typeof(IntPtr)}, typeof(ObjectHandle<T>), true);
        var il = m.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ret);
        GetO = m.CreateDelegate(typeof(GetO_d)) as GetO_d;
    }
}

/*Usage*/
using(var handle = new ObjectHandle<MyClass>())
{
    //do some work
}

希望它能对您有所帮助.

I hope it will help you on your path.

找到了引用类型字段的解决方案:

class MyClass
{
    private IntPtr a_ptr;
    public object a{
        get{
            return Voodoo<object>.GetO(a_ptr);
        }
        set{
            a_ptr = Voodoo<object>.GetPtr(value);
        }
    }
    public int b;
    public int c;
}

更好的解决方案.只需使用 ObjectContainer< object> 而不是 object 等.

Even better solution. Just use ObjectContainer<object> instead of object and so on.

public struct ObjectContainer<T> where T : class
{
    private readonly T val;

    public ObjectContainer(T obj)
    {
        val = obj;
    }

    public T Value{
        get{
            return val;
        }
    }

    public static implicit operator T(ObjectContainer<T> @ref)
    {
        return @ref.val;
    }

    public static implicit operator ObjectContainer<T>(T obj)
    {
        return new ObjectContainer<T>(obj);
    }

    public override string ToString()
    {
        return val.ToString();
    }

    public override int GetHashCode()
    {
        return val.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        return val.Equals(obj);
    }
}