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

时间:2012-05-29 13:21:40

标签: c# .net garbage-collection clr unmanaged-memory

更新:现在有一个已接受的答案“有效”。你永远都不应该永远使用它。 自从


首先让我先说明我是游戏开发者。想要这样做有一个合理的 - 如果非常不寻常 - 与性能相关的原因。


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

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

没什么特别的。请注意,它是仅包含值类型的引用类型。

在托管代码中,我希望有这样的内容:

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

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

基本上:有没有办法 - 无论多么苛刻 - 将一些完全随意的指针转换为对象引用。而且 - 没有让CLR爆炸 - 该死的后果。

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

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

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

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


更新:到目前为止,这是“缺失的部分”:

#1:如何将IntPtr转换为对象引用?虽然无法验证的IL可能是可能的(见评论)。到目前为止,我没有运气。该框架似乎非常小心,以防止这种情况发生。

(能够在运行时获取非blittable托管类型的大小和布局信息也是很好的。再一次,框架试图使这变得不可能。)

#2:假设问题可以解决 - 当GC遇到指向GC堆外部的对象引用时,它会做什么?它崩溃了吗? Anton Tykhyy,in his answer,猜测它会。考虑到框架对于防止#1的谨慎程度,它似乎很可能。确认这一点的事情会很好。

(或者,对象引用可以指向GC堆内的固定内存。这会有所不同吗?)

基于此,我倾向于认为这种黑客攻击的想法是不可能的 - 或者至少不值得努力。但我有兴趣得到#1或#2或两者的技术细节的答案。

8 个答案:

答案 0 :(得分:7)

  

“我想为C#实现自定义分配器”

GC是CLR的核心。只有Microsoft(或Mono的Mono团队)可以替代它,但开发工作成本很高。 GC是CLR的核心,搞乱GC或托管堆会使CLR崩溃 - 如果你非常幸运的话,很快就会崩溃。

  

当遇到指向托管内存之外的引用时,垃圾收集器会做什么(特定于实现,如果需要)?

它以特定于实现的方式崩溃;)

答案 1 :(得分:6)

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

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。 如果您想要更复杂的解决方案,可以试试这个:

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
}

我希望它会帮助你走上正轨。

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

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,依此类推。

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);
    }
}

答案 2 :(得分:3)

纯粹的C#方法

所以,有一些选择。最简单的方法是在结构的不安全上下文中使用new / delete。第二种是使用内置的编组服务来处理非托管内存(代码见下文)。然而,这两个都处理结构(虽然我认为后一种方法非常接近你想要的)。我的代码有一个限制,你必须始终坚持结构并使用IntPtrs作为引用(使用ChunkAllocator.ConvertPointerToStructure获取数据,使用ChunkAllocator.StoreStructure存储更改的数据)。这显然很麻烦,所以如果你使用我的方法,你最好真的想要性能。但是,如果您只处理 值类型,则此方法就足够了。

绕道:CLR中的类

类在其分配的内存中有一个8字节的“前缀”。四个字节用于多线程的同步索引,四个字节用于标识它们的类型(基本上是虚方法表和运行时反射)。这使得很难处理非托管内存,因为这些是特定于CLR的,因为同步索引可以在运行时更改。有关运行时对象创建的详细信息,请参阅here;有关引用类型的内存布局概述,请参阅here。另请查看CLR via C#以获得更深入的解释。

警告

像往常一样,事情很少像是/否那么简单。引用类型的真正复杂性与垃圾收集器在垃圾收集期间如何压缩分配的内存有关。如果你能以某种方式确保垃圾收集不会发生,或者它不会影响有问题的数据(参见fixed keyword),那么你可以将任意指针转换为对象引用(只需通过8个字节,然后将该数据解释为具有相同字段和内存布局的结构;也许使用StructLayoutAttribute来确定。我会尝试使用非虚方法来查看它们是否有效;他们应该(特别是如果你把它们放在结构上)但由于你必须丢弃的虚方法表,虚拟方法是不可行的。

一个人不会简单地走进Mordor

简单地说,这意味着无法在非托管内存中分配托管引用类型(类)。您可以在C ++中使用托管引用类型,但这些类型将受垃圾回收...而且流程和代码比基于struct的方法更痛苦。那会给我们留下什么?当然,回到我们开始的地方。

有一种秘密方式

我们可以自己勇敢地<罢工> Shelob's Lair 内存分配。不幸的是,这是我们的路径必须分开的地方,因为我不是那么了解它。我会实际为您提供linktwo - 或许threefour。这是相当复杂的并且提出了一个问题:您是否可以尝试其他优化?缓存一致性和优越算法是一种方法,P / Invoke对性能关键代码的明智应用也是如此。您还可以为关键方法/类应用上述仅结构内存分配。

祝你好运,如果你找到了更好的选择,请告诉我们。

附录:源代码

ChunkAllocator.cs

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

namespace MemAllocLib
{
    public sealed class ChunkAllocator : IDisposable
    {
        IntPtr m_chunkStart;
        int m_offset;//offset from already allocated memory
        readonly int m_size;

        public ChunkAllocator(int memorySize = 1024)
        {
            if (memorySize < 1)
                throw new ArgumentOutOfRangeException("memorySize must be positive");

            m_size = memorySize;
            m_chunkStart = Marshal.AllocHGlobal(memorySize);
        }
        ~ChunkAllocator()
        {
            Dispose();
        }

        public IntPtr Allocate<T>() where T : struct
        {
            int reqBytes = Marshal.SizeOf(typeof(T));//not highly performant
            return Allocate<T>(reqBytes);
        }

        public IntPtr Allocate<T>(int reqBytes) where T : struct
        {
            if (m_chunkStart == IntPtr.Zero)
                throw new ObjectDisposedException("ChunkAllocator");
            if (m_offset + reqBytes > m_size)
                throw new OutOfMemoryException("Too many bytes allocated: " + reqBytes + " needed, but only " + (m_size - m_offset) + " bytes available");

            T created = default(T);
            Marshal.StructureToPtr(created, m_chunkStart + m_offset, false);
            m_offset += reqBytes;

            return m_chunkStart + (m_offset - reqBytes);
        }

        public void Dispose()
        {
            if (m_chunkStart != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(m_chunkStart);
                m_offset = 0;
                m_chunkStart = IntPtr.Zero;
            }
        }

        public void ReleaseAllMemory()
        {
            m_offset = 0;
        }

        public int AllocatedMemory
        {
            get { return m_offset; }
        }

        public int AvailableMemory
        {
            get { return m_size - m_offset; }
        }

        public int TotalMemory
        {
            get { return m_size; }
        }

        public static T ConvertPointerToStruct<T>(IntPtr ptr) where T : struct
        {
            return (T)Marshal.PtrToStructure(ptr, typeof(T));
        }

        public static void StoreStructure<T>(IntPtr ptr, T data) where T : struct
        {
            Marshal.StructureToPtr(data, ptr, false);
        }
    }
}

Program.cs的

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MemoryAllocation
{
    class Program
    {
        static void Main(string[] args)
        {
            using (MemAllocLib.ChunkAllocator chunk = new MemAllocLib.ChunkAllocator())
            {
                Console.WriteLine(">> Simple data test");
                SimpleDataTest(chunk);

                Console.WriteLine();

                Console.WriteLine(">> Complex data test");
                ComplexDataTest(chunk);
            }

            Console.ReadLine();
        }

        private static void SimpleDataTest(MemAllocLib.ChunkAllocator chunk)
        {
            IntPtr ptr = chunk.Allocate<System.Int32>();

            Console.WriteLine(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr));
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr) == 0, "Data not initialized properly");
            System.Diagnostics.Debug.Assert(chunk.AllocatedMemory == sizeof(Int32), "Data not allocated properly");

            int data = MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr);
            data = 10;
            MemAllocLib.ChunkAllocator.StoreStructure(ptr, data);

            Console.WriteLine(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr));
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Int32>(ptr) == 10, "Data not set properly");

            Console.WriteLine("All tests passed");
        }

        private static void ComplexDataTest(MemAllocLib.ChunkAllocator chunk)
        {
            IntPtr ptr = chunk.Allocate<Person>();

            Console.WriteLine(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr));
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr).Age == 0, "Data age not initialized properly");
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr).Name == null, "Data name not initialized properly");
            System.Diagnostics.Debug.Assert(chunk.AllocatedMemory == System.Runtime.InteropServices.Marshal.SizeOf(typeof(Person)) + sizeof(Int32), "Data not allocated properly");

            Person data = MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr);
            data.Name = "Bob";
            data.Age = 20;
            MemAllocLib.ChunkAllocator.StoreStructure(ptr, data);

            Console.WriteLine(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr));
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr).Age == 20, "Data age not set properly");
            System.Diagnostics.Debug.Assert(MemAllocLib.ChunkAllocator.ConvertPointerToStruct<Person>(ptr).Name == "Bob", "Data name not set properly");

            Console.WriteLine("All tests passed");
        }

        struct Person
        {
            public string Name;
            public int Age;

            public Person(string name, int age)
            {
                Name = name;
                Age = age;
            }

            public override string ToString()
            {
                if (string.IsNullOrWhiteSpace(Name))
                    return "Age is " + Age;
                return Name + " is " + Age + " years old";
            }
        }
    }
}

答案 3 :(得分:2)

您可以使用P / Invoke在C ++中编写代码并使用.NET调用它,或者您可以在托管C ++中编写代码,从而使您可以从.NET语言内完全访问本机API。但是,在托管端,您只能使用托管类型,因此您必须封装非托管对象。

举一个简单的例子:Marshal.AllocHGlobal允许您在Windows堆上分配内存。返回的句柄在.NET中没什么用处,但在调用需要缓冲区的本机Windows API时可能需要它。

答案 4 :(得分:2)

这是不可能的。

但是,您可以使用托管结构并创建此结构类型的指针。该指针可以指向任何地方(包括非托管内存)。

问题是,为什么要在非托管内存中拥有一个类?无论如何你都不会得到GC功能。你可以使用指向结构的指针。

答案 5 :(得分:0)

这样的事情是可能的。您可以在不安全的上下文中访问托管内存,但所述内存仍然受管理并受GC限制。

  

为什么?

简单和安全。

但是现在我考虑一下,我认为你可以将托管和非托管与C ++ / CLI混合使用。但我不确定,因为我从未使用过C ++ / CLI。

答案 6 :(得分:0)

我不知道如何在非托管堆中保存C#类实例,即使在C ++ / CLI中也是如此。

答案 7 :(得分:0)

可以在.net中完全设计一个值类型分配器,而不使用任何非托管代码,它可以分配和释放任意数量的值类型实例,而不会产生任何显着的GC压力。诀窍是创建一个相对较少数量的数组(可能每个类型一个)来保存实例,然后传递“实例引用”结构,其中包含相关索引的数组索引。

假设,例如,我想拥有一个“生物”类,它拥有XYZ位置(float),XYZ速度(也float),滚动/俯仰/偏航(同上),损坏(浮动)和种类(枚举)。接口“ICreatureReference”将为所有这些属性定义getter和setter。典型的实现是具有单个私有字段CreatureReference的结构int _index和属性访问器,如:

  float Position {
    get {return Creatures[_index].Position;} 
    set {Creatures[_index].Position = value;}
  };

系统将保留一个列表,列出使用哪些数组时隙和空闲(如果需要,可以使用Creatures中的一个字段来形成空闲时隙的链接列表)。 CreatureReference.Create方法将从空置项列表中分配项目; Dispose实例的CreatureReference方法会将其数组槽添加到空置项列表中。

这种方法最终需要大量的样板代码,但它可以合理有效并避免GC压力。最大的问题可能是(1)它使structs的行为更像引用类型而不是structs,并且(2)它需要绝对的约束与调用IDispose,因为非处置数组插槽永远不会被收回。另一个令人厌烦的怪癖是,即使属性设置者不会尝试改变它们所在的CreatureReference实例的任何字段,也不能将属性设置器用于CreatureReference类型的只读值。应用。使用接口ICreatureReference可以避免这种困难,但必须注意仅声明约束为ICreatureReference的泛型类型的存储位置,而不是声明ICreatureReference的存储位置。