Страницы

Поиск по вопросам

среда, 22 января 2020 г.

Изменить размер массива через unsafe

#c_sharp #массивы #unity3d #unsafe


Экскурс к тому, для чего нужно:

Я работаю с Unity, и вынужден пользоваться его API. 

Среди API есть метод AssetBundle.LoadFromMemory(byte[] bytes), причем перегрузок
с параметрами offset, length там нет.

Этот метод используется для чтения файлов из памяти, однако мои файлы упакованы в
самописный архив, который я хотел читать через кешируемый буфер, чтобы не порождать
аллокации при создании массива.

То есть я буквально заперт тем, что мне диктует API. Я не могу например сохранить
размер массива отдельно и вынужден копировать данные в новый отдельный массив всякий
раз, когда мне нужно воспользоваться данным методом.

Я бы хотел найти какой-нибудь способ обмануть метод API, протолкнув ему мой кешируемый
буфер заведомо большего размера. Информация в этом кешируемом буфере просто перезаписывается
поверх старой информации, а переразмер происходит только когда данных больше. 

Для этих целей я нафантазировал "подменить" размер массива на меньший, а после вызова
метода вернуть размер обратно. Буквально хотелось бы просто изменить число, которое
возвращает Array.Length, при том количество данных в нем оставить прежним.

Возможно ли как-то сделать это через unsafe?

Любые другие идеи приветствуются.
    


Ответы

Ответ 1



Изменить размер можно, он хранится сразу перед первым элементом массива (проверял на CLR 2, 4, CoreCLR). static unsafe void Main(string[] args) { // для примера var array = new byte[] { 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa }; Print(array); int oldLength = array.Length; int newLength = 4; // адрес начала массива fixed (byte* ptr = array) { // адрес, по которому хранится размер массива // для x86 он равен ptr - 4, для x64 ptr - 8; int* pSize = (int*)(ptr - sizeof(void*)); // устанавливаем новый размер (только в сторону уменьшения) *pSize = newLength; Print(array); // здесь используйте массив в Unity... // возвращаем старый размер *pSize = oldLength; Print(array); } } static void Print(byte[] array) { Console.WriteLine("array.Length == " + array.Length); foreach (byte a in array) Console.Write(a.ToString("X2") + " "); Console.WriteLine(); Console.WriteLine(); } В результате функция Print выведет: // исходный массив array.Length == 10 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA // измененный массив array.Length == 4 A1 A2 A3 A4 // восстановленный массив array.Length == 10 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA Чтобы добавить смещение, нужно циклически сдвинуть массив влево на величину смещения, затем отрезать длину до нужной. Восстановление - в обратном порядке, сначала изменяем длину до оригинальной, затем двигаем массив циклически вправо.

Ответ 2



Исходя из задачи всётаки двигать byte[] это можно сделать так... В памяти структура такая: СсылкаНаКласс - Размер - Данные. Ссылку на структуру, как и саму структуру можно менять. Но в процессе работы выяснилось, что обнамуть "указатель" не удаётся, и для смещения позиции таки нужно кастить gc.Target что б изменения вошли в силу. Вот пример реализации: static byte[] ReArray(GCHandle garray, int offs, int size) { IntPtr ppArray = GCHandle.ToIntPtr(garray);/*ссылка на ссылку (массив)*/ IntPtr pArray = Marshal.ReadIntPtr(ppArray);/*ссылка на тело*/ IntPtr ArrayClass = Marshal.ReadIntPtr(pArray);/*Ссылка на класс*/ Marshal.WriteIntPtr(pArray, offs, ArrayClass); /*Запись ссылки на класс*/ Marshal.WriteInt32(pArray, offs + IntPtr.Size, size);/*Запись размера*/ /*Запишем новую ссылку на сам массив*/ Marshal.WriteIntPtr(ppArray, (IntPtr.Size==4)?new IntPtr(pArray.ToInt32()+offs) : new IntPtr( pArray.ToInt64() + offs )); return (byte[])gc.Target;/*вернём сам массив (новый)*/ } //------------------------------------------------- GCHandle gc = GCHandle.Alloc(bytes); /*Берём массив*/ Your_Handler( ReArray(gc, 0, 4) ); /*Укоротим*/ Your_Handler( ReArray(gc, 4, 4) ); /*А теперь сложнее - сместим*/ Your_Handler( ReArray(gc, 8, 2) ); ReArray(gc, -12, 2); /*Вернуть позицию на "место" обязательно, сумма смещений 4+8 */ gc.Free(); Тут идея что offs - смещает массив "вперёд-назад", но 12 байт по адресу ppArray нужно потом вернуть назад (я так думаю что если не вернуть то возможен глюк). При смещении массива, если работать с "bytes" то в JIT-коде остаются прямые ссылки на pArray - и bytes начинает глючить (хотя фактически он обновился), но если сделать сast Target-у - "возвращается" bytes на заданное место (обновляет). Для х64 не тестил. Ещё под вопросом можно ли offs брать не кратным 4 или 8 (x64). Вроде работает, но будет ли исключение "unaligned read" не могу сказать. Как достоинство - unsafe можно избежать, но решение небезопасное. Понятно нужно расчитывать что для "шапки" при смещении несколько байт "вытираются". "Легальный" путь создать массив на базе другого: byte[] data = GetData(); // Получить массив который нужно разшить your_handler( (new MemoryStream ( data, xfrom , xlen)).ToArray()); Но он более требовательный по-памяти. Если есть перезагрузка использующая Stream - то можно использовать.

Комментариев нет:

Отправить комментарий