Как создать экземпляр класса C# в неуправляемой памяти? (Возможно?)
UPDATE: теперь есть принятый ответ, который"работает". Никогда, никогда, никогда!--7-->когда-нибудь использовать его. когда-нибудь.
сначала позвольте мне предварить мой вопрос, заявив, что я разработчик игры. Есть законная, хотя и очень необычная, причина, по которой вы хотите это сделать.
скажем у меня есть класс 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?)
в принципе: есть ли способ - независимо от того, насколько hacky-превратить какой-то совершенно произвольный указатель в ссылку на объект. И - за исключением того, чтобы взорвать CLR-будь прокляты последствия.
(другой способ поставить мой вопрос: "я хотите реализовать пользовательский распределитель для C#")
это приводит к следующему вопросу: Что делает сборщик мусора (специфичный для реализации, если это необходимо) при столкновении со ссылкой, которая указывает за пределами управляемой памяти?
и, в связи с этим, что произойдет, если Foo
была ссылка в качестве поля-члена? Что, если он указывает на управляемую память? Что, если он когда-либо указывал только на другие объекты, выделенные в неуправляемой памяти?
наконец, если этот невозможно: Почему?
обновление: вот "недостающие части" до сих пор:
#1: как преобразовать IntPtr
ссылка на объект? Это может быть возможно, хотя и непроверяемый IL (см. комментарии). До сих пор мне не везло. Рамки, по-видимому, чрезвычайно осторожны, чтобы предотвратить это.
(также было бы неплохо получить информацию о размере и макете для не-blittable управляемых типов на во время выполнения. Опять же, фреймворк пытается сделать это невозможным.)
#2: предполагая, что проблема может быть решена - что делает GC, когда он сталкивается с ссылкой на объект, который указывает за пределами кучи GC? Он разбивается? Антон Тихий,в ответ, догадки, что это будет. Учитывая, насколько тщательна структура для предотвращения #1, это кажется вероятным. Что-то, что подтверждает это, было бы неплохо.
(в качестве альтернативы ссылка на объект может указывать закрепить память внутри кучи GC. Будет ли это иметь значение?)
основываясь на этом, я склонен думать, что эта идея для взлома невозможна - или, по крайней мере, не стоит усилий. Но мне было бы интересно получить ответ, который входит в технические детали #1 или #2 или обоих.
8 ответов
я экспериментировал с созданием классов в неуправляемой памяти. Это возможно, но есть проблема, которую я в настоящее время не могу решить - вы не можете назначать объекты полям ссылочного типа - см. редактирование внизу -, так что вы можете иметь только поля структуры в пользовательском классе.
это зло:
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
}
Я надеюсь, что это поможет вам на вашем пути.
Edit: найдено решение для полей ссылочного типа:
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;
}
Edit: еще лучшее решение. Просто используйте 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 для ссылок (используя Чанкаллокатор.ConvertPointerToStructure для получения данных и ChunkAllocator.StoreStructure для хранения измененных данных). Это, очевидно, громоздко, так что вам лучше действительно хотите производительность, если вы используете мой подход. Однако, если вы имеете дело с только value-types, этого подхода достаточно.
объезд: классы в CLR
классы имеют 8-байтовый "префикс" в выделенной памяти. Четыре байта для индекса синхронизации для многопоточность и четыре байта предназначены для идентификации их типа (в основном, таблица виртуальных методов и отражение во время выполнения). Это затрудняет работу с неуправляемой памятью, поскольку они зависят от среды CLR и поскольку индекс синхронизации может изменяться во время выполнения. См.здесь для получения подробной информации о создании объекта во время выполнения и здесь для обзора макета памяти для ссылочного типа. Кроме того, проверить CLR через C# для более углубленного объяснение.
Предостережение
как обычно, все не так просто, как Да/нет. Реальная сложность ссылочных типов связана с тем, как сборщик мусора сжимает выделенную память во время сбора мусора. Если вы можете каким-то образом гарантировать, что сбор мусора не произойдет или что он не повлияет на данные, о которых идет речь (см. исправлено ключевое слово) затем вы можете превратить произвольный указатель в ссылку на объект (просто сдвиньте указатель на 8 байт, затем интерпретируйте эти данные как структуру с теми же полями и макетом памяти; возможно, используйте атрибута structlayoutattribute конечно). Я бы экспериментировал с не виртуальными методами, чтобы увидеть, работают ли они; они должны (особенно если вы поместите их в структуру), но виртуальные методы не работают из-за таблицы виртуальных методов, которую вам придется отбросить.
Нельзя Просто Так Войти В Мордор
проще говоря, это означает, что управляемые ссылочные типы (классы) не может быть выделяется в неуправляемой памяти. Вы можете использовать управляемые ссылочные типы В C++, но они будут подлежать сбору мусора... и процесс и код более болезненны, чем struct
-ориентированного подхода. Что это нам дает? Туда, откуда мы начали, конечно.
есть секретный путь
мы могли бы смело Шелобой логово выделение памяти сами. К сожалению, именно здесь наши пути должны разойтись, потому что я не настолько осведомлен об этом. Я предоставит вам ссылке или два - возможно,три или четыре на самом деле. Это довольно сложно и напрашивается вопрос: есть ли другие оптимизации, которые вы могли бы попробовать? Согласованность кэша и превосходные алгоритмы-это один из подходов, как и разумное применение 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);
}
}
}
.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";
}
}
}
}
вы можете написать код на C++ и вызвать его из .NET с помощью P/Invoke или вы можете написать код на управляемом C++, который дает вам полный доступ к родному API изнутри языка .NET. Однако на управляемой стороне вы можете работать только с управляемыми типами, поэтому вам придется инкапсулировать неуправляемые объекты.
простой пример: Маршал.AllocHGlobal позволяет выделить память в куче Windows. Дескриптор, возвращенный не в .Чистая, но может потребоваться при вызове собственного API Windows, требующего буфера.
Это невозможно.
однако вы можете использовать управляемая структура и создайте указатель этого типа структуры. Этот указатель может указывать в любом месте (включая неуправляемую память).
вопрос в том, почему вы хотите иметь класс в неуправляемой памяти? Вы все равно не получите функции GC. Вы можете просто использовать указатель на структуру.
ничего подобного не возможно. Вы можете получить доступ к управляемой памяти в небезопасном контексте, но эта память по-прежнему управляется и подлежит GC.
Почему?
простота и безопасность.
но теперь, когда я думаю об этом, я думаю, что вы можете смешивать управляемое и неуправляемое с C++/CLI. Но я не уверен в этом, потому что я никогда не работал с C++/CLI.
можно создать распределитель типа значения полностью в .net без использования неуправляемого кода, который может выделять и освобождать произвольное количество экземпляров типа значения без какого-либо значительного давления GC. Трюк состоит в том, чтобы создать относительно небольшое количество массивов (возможно, по одному для каждого типа) для хранения экземпляров, а затем передать структуры "ссылки на экземпляр", которые содержат индексы массива рассматриваемого индекса.
предположим, например, что я хочу имейте класс "существо", который содержит позиции XYZ (float
), скорость XYZ (также float
), крен/тангаж/рыскание (ditto), повреждение (поплавок), и вид (перечисление). Интерфейс "ICreatureReference" определит геттеры и сеттеры для всех этих свойств. Типичной реализацией будет struct CreatureReference
С одним частным полем int _index
и аксессоры свойств, такие как:
float Position { get {return Creatures[_index].Position;} set {Creatures[_index].Position = value;} };
система сохранит список используемых и свободных слотов массива (при желании можно использовать одно из полей внутри Creatures
для формирования связанного списка свободных слотов). The CreatureReference.Create
метод выделит элемент из списка вакантных элементов;Dispose
метод a CreatureReference
экземпляр добавит свой слот массива в список вакантных элементов.
этот подход в конечном итоге требует раздражающего количества кода шаблона, но он может быть достаточно эффективным и избегать давления GC. Самые большие проблемы, вероятно, в том, что (1) это делает structs
вести себя как ссылочные типы, чем structs
, и (2) это требует абсолютной дисциплины с вызовом IDispose
, так как не утилизированные слоты массива никогда не будут утилизированы. Еще одна досадная причуда заключается в том, что невозможно использовать установщики свойств для значений типа CreatureReference
, даже если установщики свойств не будут пытаться мутировать какие-либо поля CreatureReference
экземпляр, к которому они применяются. Использование интерфейса ICreatureReference
может избежать этой трудности, но нужно быть осторожным, чтобы объявить только места хранения общих типов принужден к ICreatureReference
, а не объявлять места хранения ICreatureReference
.