Добрый день, уважаемые разработчики (просто не знал, с чего начать пост). Предлагаю перед тем как начнется трудовая неделя немного подразмять
Как все мы знаем, в .Net существует две группы куч: для больших и малых объектов. Как выяснить, во сколько нам обойдется объект можно при помощи кода из этой статьи (он нам пригодится): Ручное клонирование потока, а получить указатель на объект и по указателю получить сам объект можно научиться, прочтя эту статью: Получение указателя на объект .Net. Также нам понадобится статья корейского (южно-) программиста по перенаправлению указателя на скомпилированную часть метода на другой метод: 실행 시에 메서드 가로채기 — CLR Injection: Runtime Method Replacer 개선
Так что давайте поэкспериментируем и напишем библиотеку, которая позволит:
- Аллоцировать участок памяти
- Разметить его как набор объектов определенного .Net типа
- Выделять объекты с этой памяти
- Возвращать их обратно
Т.е. напишем пул объектов вне .Net памяти.
Будем решать задачи по мере поступления вопросов.
- Как разметить уже саллоцированный участок памяти каким-либо объектом?
В любом объектно-ориентированном языке объект состоит из указателя на таблицу виртуальных методов и полей объекта. Таблица эта необходима чтобы понимать какие из перегрузок методов должны быть вызваны и сама по себе нам не так интересна в рамках данной задачи. Если мы работаем с каким-либо объектом и вызываем «у него» метод, это значит что сначала будет загружен адрес объекта, по нему будем расположен адрес таблицы виртуальных методов. А по этому адресу мы берем указатель на нужный метод и вызываем его. Значит чтобы любой участок памяти представить как объект определенного типа необходимо просто записать указатель на таблицу виртуальных методов.Как мы это можем сделать? Сначала я брал экземпляр существующего объекта, и вычитывал первые 4 или 8 байт, записывая их к себе. Метод работал, но он не красивый. Нужен экземпляр. После чего я нашел что этот адрес легко вычитывается с помощью свойства typeof(TType).TypeHandle.
- Как выделить кусок памяти?
Это сделать совсем просто: есть функцияMarshal.AllocHGlobal(_totalSize), которая позволяет выделить любое требуемое количество виртуальной памяти. Если вам при этом надо разместить эту память по любому адресу, то надо воспользоваться ее WinApi аналогом. - А как же вызов конструктора?
Для того чтобы вызвать конструктор, у нас три пути:- Сделать метод Init и вызывать его. Это не очень красиво, не очень спортивно. Однако, не надо лезть в рефлексию и во внутренности .Net CLR.
- Вызывать конструктор через рефлексию. Более спортивный метод, однако рефлексия налагает определенные тормоза.
- После редактирования таблицы скомпилированных тел методов вызвать другой метод, но при этом будет вызван конструктор. Это мозговыносящий метод и в нем – наибольший процент спорта =) Им и воспользуемся. Ведь получится прямой вызов, без посредников. Как будто конструктор и вызвали.
Ну что, готовы? Давайте приступим.
Первое, что мы определяем – типы
internal static class Stub
{
public static void Construct(object obj, int value)
{
}
}
Это — тип, единственный статический метод которого будет изменен так, что указатель на скомпилированную часть метода будет смотреть на конструктор нашего типа. Первый параметр — obj — является по своей сути указателем this. Ведь, как вы знаете, this есть ни что иное как первый параметр метода, который есть всегда в каждом экземплярном методе. Второй параметр — целое число. Введен для того чтобы проверить что мы можем передавать целые числа.
public class UnmanagedObject<T> : IDisposable where T : UnmanagedObject<T>
{
internal IUnmanagedHeap<T> heap;
#region IDisposable implementation
void IDisposable.Dispose()
{
heap.Free(this);
}
#endregion
}
Далее введем тип UnmanagedObject чтобы во-первых ввести метод возврата объекта в пул Dispose(), а во-вторых архитектурно отделить все объекты, предназначенные для размещения вне CLR куч от стандартных. Единственное поле класса типа internal, чтобы его можно было задать извне, в пуле объектов.
И последнее — класс самого пула.
public unsafe class UnmanagedHeap<TPoolItem> : IUnmanagedHeap<TPoolItem> where TPoolItem : UnmanagedObject<TPoolItem>
{
private readonly IntPtr *_freeObjects;
private readonly IntPtr *_allObjects;
private readonly int _totalSize, _capacity;
private int _freeSize;
private readonly void *_startingPointer;
private readonly ConstructorInfo _ctor;
public UnmanagedHeap(int capacity)
{
_freeSize = capacity;
// Getting type size and total pool size
var objectSize = GCEx.SizeOf<TPoolItem>();
_capacity = capacity;
_totalSize = objectSize * capacity + capacity * IntPtr.Size * 2;
_startingPointer = Marshal.AllocHGlobal(_totalSize).ToPointer();
var mTable = (MethodTableInfo*)typeof(TPoolItem).TypeHandle.Value.ToInt32();
_freeObjects = (IntPtr*)_startingPointer;
_allObjects = (IntPtr*)((long)_startingPointer + IntPtr.Size * capacity);
_startingPointer = (void*)((long)_startingPointer + 2 * IntPtr.Size * capacity);
var pFake = typeof(Stub).GetMethod("Construct", BindingFlags.Static|BindingFlags.Public);
var pCtor = _ctor = typeof(TPoolItem).GetConstructor(new []{typeof(int)});
MethodUtil.ReplaceMethod(pCtor, pFake, skip: true);
for(int i = 0; i < capacity; i++)
{
var handler = (IntPtr *)((long)_startingPointer + (objectSize * i));
handler[1] = (IntPtr)mTable;
var obj = EntityPtr.ToInstance<object>((IntPtr)handler);
var reference = (TPoolItem)obj;
reference.heap = this;
_allObjects[i] = (IntPtr)(handler + 1);
}
Reset();
}
public int TotalSize
{
get {
return _totalSize;
}
}
public TPoolItem Allocate()
{
_freeSize--;
var obj = _freeObjects[_freeSize];
Stub.Construct(obj, 123);
return EntityPtr.ToInstanceWithOffset<TPoolItem>(obj);
}
public void Free(TPoolItem obj)
{
_freeObjects[_freeSize] = EntityPtr.ToPointerWithOffset(obj);
_freeSize++;
}
public void Reset()
{
WinApi.memcpy((IntPtr)_freeObjects, (IntPtr)_allObjects, _capacity * IntPtr.Size);
_freeSize = _capacity;
}
object IUnmanagedHeapBase.Allocate()
{
return this.Allocate();
}
void IUnmanagedHeapBase.Free(object obj)
{
this.Free((TPoolItem)obj);
}
public void Dispose()
{
Marshal.FreeHGlobal((IntPtr)_freeObjects);
}
}
По порядку:
В конструкторе класса мы сначала рассчитываем размер экземпляра типа. После чего умножаем на capacity, добавляем размер таблиц свободных/занятых слотов и получаем размер пула. Получив размер, аллоцируем пул в виртуальной памяти. После чего получаем описатели методов: конструктора типа и заглушки и у заглушки выставляем указатель на тело метода как тело конструктора:
var pFake = typeof(Stub).GetMethod("Construct", BindingFlags.Static|BindingFlags.Public);
var pCtor = _ctor = typeof(TPoolItem).GetConstructor(new []{typeof(int)});
MethodUtil.ReplaceMethod(pCtor, pFake, skip: true);
Последнее — в цикле, у каждого будущего объекта проставляем указатель на таблицу вирт методов, делаем кастинг в .Net тип и выставляем поле heap у только что проинициализированного объекта в наш пул.
Отдельный интерес представляет метод Allocate:
public TPoolItem Allocate()
{
_freeSize--;
var obj = _freeObjects[_freeSize];
Stub.Construct(obj, 123);
return EntityPtr.ToInstanceWithOffset<TPoolItem>(obj);
}
В нем мы сначала из таблицы свободных объектов берем последний из них. После чего вызываем метод Construct класса Stub, тело которого на самом деле — наш конструктор класса элемента пула. Конструктору передаем число 123 как параметр.
Использование
Использование протестируем с помощью следующего кода
using System;
using System.Runtime.CLR;
namespace UnmanagedPoolSample
{
class Program
{
/// <summary>
/// Now cannot call default .ctor
/// </summary>
private class Quote : UnmanagedObject<Quote>
{
public Quote(int urr)
{
Console.WriteLine("Hello from object .ctor");
}
public int GetCurrent()
{
return 100;
}
}
static void Main(string[] args)
{
using (var pool = new UnmanagedHeap<Quote>(1000))
{
using (var quote = pool.Allocate())
{
Console.WriteLine("quote: {0}", quote.GetCurrent());
}
}
Console.ReadKey();
}
}
}
Вывод в консоль: