Продолжаем кромсать CLR: пул объектов .Net вне куч SOH/LOH

Добрый день, уважаемые разработчики (просто не знал, с чего начать пост). Предлагаю перед тем как начнется трудовая неделя немного подразмять мозги (совсем немного) и построить свой Small Objects Heap для .Net. Вернее даже не Small Objects Heap, а Custom Objects Heap. Это — один из примеров прошлогодней конференции CLRium, новая итерация которой состоится 7d19d78ab896445fb5573b5f5e807ba0.png в Апреле — в Москве и в Мае — в ПитереКак все мы знаем, в .Net существует две группы куч: для больших и малых объектов. Как выяснить, во сколько нам обойдется объект можно при помощи кода из этой статьи (он нам пригодится): 7d19d78ab896445fb5573b5f5e807ba0.png Ручное клонирование потока, а получить указатель на объект и по указателю получить сам объект можно научиться, прочтя эту статью: 7d19d78ab896445fb5573b5f5e807ba0.png Получение указателя на объект .Net. Также нам понадобится статья корейского (южно-) программиста по перенаправлению указателя на скомпилированную часть метода на другой метод: 실행 시에 메서드 가로채기 — CLR Injection: Runtime Method Replacer 개선

Так что давайте поэкспериментируем и напишем библиотеку, которая позволит:

Аллоцировать участок памяти Разметить его как набор объектов определенного .Net типа Выделять объекты с этой памяти Возвращать их обратно Ссылка на проект на GitHub: 97938edf1170406c842b0613d0c84ae9.png DotNetExТ.е. напишем пул объектов вне .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: IDisposable where T: UnmanagedObject { internal IUnmanagedHeap heap; #region IDisposable implementation void IDisposable.Dispose () { heap.Free (this); } #endregion } Далее введем тип UnmanagedObject чтобы во-первых ввести метод возврата объекта в пул Dispose (), а во-вторых архитектурно отделить все объекты, предназначенные для размещения вне CLR куч от стандартных. Единственное поле класса типа internal, чтобы его можно было задать извне, в пуле объектов.И последнее — класс самого пула.

public unsafe class UnmanagedHeap: IUnmanagedHeap where TPoolItem: UnmanagedObject { 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(); _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((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(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(obj); }

В нем мы сначала из таблицы свободных объектов берем последний из них. После чего вызываем метод Construct класса Stub, тело которого на самом деле — наш конструктор класса элемента пула. Конструктору передаем число 123 как параметр.Использование Использование протестируем с помощью следующего кодаusing System; using System.Runtime.CLR; namespace UnmanagedPoolSample { class Program { ///

/// Now cannot call default .ctor /// private class Quote: UnmanagedObject { 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(1000)) { using (var quote = pool.Allocate ()) { Console.WriteLine («quote: {0}», quote.GetCurrent ()); } }

Console.ReadKey (); } } } Вывод в консоль: 50dea013b5f54f19b94a4fec88626078.png

7a407b46e6d14bdf99d72dc4c90d2226.png

© Habrahabr.ru