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

本文关键字:实例化 内存 | 更新日期: 2023-09-27 18:35:34

>更新:现在有一个公认的答案"有效"。你永远不应该,永远,永远,永远使用它。曾经


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


假设我有一个这样的 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,但这可能是可能的(请参阅注释)。到目前为止,我对此没有运气。该框架似乎非常小心地防止这种情况发生。

(能够在运行时获取非 blitable 托管类型的大小和布局信息也会很好。同样,框架试图使这不可能。

#2:假设问题一可以解决 - 当 GC 遇到指向 GC 堆外部的对象引用时,它会做什么?它会崩溃吗?Anton Tykhyy在他的回答中猜测会。鉴于框架对防止#1的谨慎程度,它似乎确实有可能。证实这一点的东西会很好。

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

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

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

我一直在尝试在非托管内存中创建类。这是可能的,但有一个我目前无法解决的问题 -

您无法将对象分配给引用类型字段 - 请参阅底部的编辑 - 因此您可以在自定义类中仅包含结构字段。这是邪恶的:
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);
    }
}

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

GC 是 CLR 的核心。只有Microsoft(或Mono的Mono团队)可以替换它,这在开发工作中付出了巨大的代价。GC 是 CLR 的核心,弄乱 GC 或托管堆会使 CLR 崩溃 — 如果你非常幸运的话,很快就会崩溃。

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

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

纯 C# 方法

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

绕道:CLR 中的课程

类在其分配的内存中有一个 8 字节的"前缀"。四个字节用于多线程处理的同步索引,四个字节用于标识其类型(基本上是虚拟方法表和运行时反射)。这使得处理非托管内存变得困难,因为这些内存是特定于 CLR 的,并且同步索引可能会在运行时更改。有关运行时对象创建的详细信息,请参阅此处,有关引用类型的内存布局概述,请参阅此处。另请查看通过 C# 的 CLR 以获得更深入的说明。

一个警告

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

一个人不会简单地走进魔多

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

有秘道

我们可以自己勇敢地分配谢洛布的巢穴记忆。不幸的是,这是我们的道路必须分开的地方,因为我对此并不了解。我会为您提供一两个链接 - 实际上可能是三个或四个。这相当复杂,并引出了一个问题:您是否可以尝试其他优化?缓存一致性和高级算法是一种方法,对性能关键型代码明智地应用 P/Invoke 也是如此。您还可以对关键方法/类应用上述仅结构内存分配。

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

附录:源代码

块分配器.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);
        }
    }
}

程序.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";
            }
        }
    }
}

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

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

是不可能的。

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

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

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

为什么?

简单性和安全性。

但是现在我想到了,我认为您可以将托管和非托管与 C++/CLI 混合使用。但我不确定,因为我从未使用 C++/CLI。

我不知道

在非托管堆中保存 C# 类实例的方法,甚至在 C++/CLI 中也是如此。

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

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

 浮点位置 {    获取{返回生物[_index]。位置;}    设置{生物[_index]。位置 = 值;}  };

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

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