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

225
20 сентября 2017, 09:28

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

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

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

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

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

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

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

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

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

Answer 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

Чтобы добавить смещение, нужно циклически сдвинуть массив влево на величину смещения, затем отрезать длину до нужной. Восстановление - в обратном порядке, сначала изменяем длину до оригинальной, затем двигаем массив циклически вправо.

Answer 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);
        int sz = Marshal.ReadInt32(pArray, IntPtr.Size); Console.WriteLine("sz="+sz);
        /*Запись ссылки на класс*/
        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 - то можно использовать.

READ ALSO
Проблемы паттерна `Singleton`

Проблемы паттерна `Singleton`

Изучая паттерн Singleton столкнулся с тем ,что классическая реализация данного паттерна очень хромает в плане потокобезопасности и что Lazy-реализация...

175
SSO SAML token в SOAP web service

SSO SAML token в SOAP web service

Господа, встал такой вопрос - имеется SSO SAML token примерно в таком виде -

166