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

в 19:52, , рубрики: .net, clr, clrium, Блог компании Luxoft

Добрый день, уважаемые разработчики (просто не знал, с чего начать пост). Предлагаю перед тем как начнется трудовая неделя немного подразмять мозги (совсем немного) и построить свой Small Objects Heap для .Net. Вернее даже не Small Objects Heap, а Custom Objects Heap. Это — один из примеров прошлогодней конференции CLRium, новая итерация которой состоится Продолжаем кромсать CLR: пул объектов .Net вне куч SOH-LOH - 1 в Апреле — в Москве и в Мае — в Питере

Как все мы знаем, в .Net существует две группы куч: для больших и малых объектов. Как выяснить, во сколько нам обойдется объект можно при помощи кода из этой статьи (он нам пригодится): Продолжаем кромсать CLR: пул объектов .Net вне куч SOH-LOH - 2 Ручное клонирование потока, а получить указатель на объект и по указателю получить сам объект можно научиться, прочтя эту статью: Продолжаем кромсать CLR: пул объектов .Net вне куч SOH-LOH - 3 Получение указателя на объект .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();
        }
    }
}

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

Автор: sidristij

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js